icadkit 0.3.7__tar.gz → 0.3.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. {icadkit-0.3.7 → icadkit-0.3.9}/Cargo.lock +2 -2
  2. {icadkit-0.3.7 → icadkit-0.3.9}/Cargo.toml +2 -1
  3. {icadkit-0.3.7 → icadkit-0.3.9}/PKG-INFO +44 -33
  4. {icadkit-0.3.7 → icadkit-0.3.9}/README.md +44 -33
  5. {icadkit-0.3.7 → icadkit-0.3.9}/corpus/public.jsonl +8 -8
  6. icadkit-0.3.9/crates/icad-core/src/container.rs +311 -0
  7. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/lib.rs +4 -2
  8. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/parts.rs +51 -0
  9. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/reader.rs +14 -0
  10. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/views.rs +92 -0
  11. icadkit-0.3.9/crates/icad-core/tests/container.rs +203 -0
  12. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-python/src/document.rs +129 -0
  13. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-python/src/lib.rs +1 -0
  14. {icadkit-0.3.7 → icadkit-0.3.9}/docs/api.md +72 -12
  15. {icadkit-0.3.7 → icadkit-0.3.9}/docs/changelog.md +116 -0
  16. {icadkit-0.3.7 → icadkit-0.3.9}/docs/csg.md +3 -3
  17. icadkit-0.3.9/docs/drawing.md +242 -0
  18. {icadkit-0.3.7 → icadkit-0.3.9}/docs/native.md +9 -9
  19. {icadkit-0.3.7 → icadkit-0.3.9}/docs/parts.md +22 -3
  20. {icadkit-0.3.7 → icadkit-0.3.9}/docs/preview.md +3 -3
  21. {icadkit-0.3.7 → icadkit-0.3.9}/docs/saved-bodies.md +16 -16
  22. icadkit-0.3.9/docs/shapes.md +103 -0
  23. {icadkit-0.3.7 → icadkit-0.3.9}/docs/support.md +36 -19
  24. {icadkit-0.3.7 → icadkit-0.3.9}/docs/trailer.md +3 -4
  25. {icadkit-0.3.7 → icadkit-0.3.9}/docs/viewer.md +4 -4
  26. icadkit-0.3.9/docs/writing.md +189 -0
  27. {icadkit-0.3.7 → icadkit-0.3.9}/pyproject.toml +2 -0
  28. icadkit-0.3.9/scripts/bump_version.py +132 -0
  29. icadkit-0.3.9/scripts/test_versions.py +229 -0
  30. {icadkit-0.3.7 → icadkit-0.3.9}/scripts/verify_artifacts.py +9 -7
  31. {icadkit-0.3.7 → icadkit-0.3.9}/scripts/verify_corpus.py +1 -0
  32. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/__init__.py +58 -1
  33. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_core.pyi +27 -0
  34. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_csg_occt.py +183 -39
  35. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_saved_occt.py +28 -21
  36. icadkit-0.3.9/src/icadkit/_shape_occt.py +71 -0
  37. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/cli.py +2 -0
  38. icadkit-0.3.9/src/icadkit/container.py +137 -0
  39. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/document.py +20 -0
  40. icadkit-0.3.9/src/icadkit/drawing.py +1104 -0
  41. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/models.py +18 -0
  42. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/parts.py +101 -0
  43. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/saved.py +40 -27
  44. icadkit-0.3.9/src/icadkit/shape.py +693 -0
  45. icadkit-0.3.9/src/icadkit/versions.py +74 -0
  46. icadkit-0.3.9/src/icadkit/views.py +310 -0
  47. icadkit-0.3.9/src/icadkit/write.py +1750 -0
  48. icadkit-0.3.9/tests/drawing_fixtures.py +203 -0
  49. {icadkit-0.3.7 → icadkit-0.3.9}/tests/fixture_builders.py +2 -0
  50. icadkit-0.3.9/tests/test_container.py +77 -0
  51. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_drawing.py +266 -3
  52. icadkit-0.3.9/tests/test_parts_material.py +116 -0
  53. icadkit-0.3.9/tests/test_shape.py +376 -0
  54. icadkit-0.3.9/tests/test_views_placement.py +155 -0
  55. icadkit-0.3.9/tests/test_write.py +722 -0
  56. icadkit-0.3.7/docs/drawing.md +0 -131
  57. icadkit-0.3.7/src/icadkit/drawing.py +0 -304
  58. icadkit-0.3.7/src/icadkit/views.py +0 -114
  59. icadkit-0.3.7/tests/drawing_fixtures.py +0 -94
  60. {icadkit-0.3.7 → icadkit-0.3.9}/COMMERCIAL-LICENSE.md +0 -0
  61. {icadkit-0.3.7 → icadkit-0.3.9}/LICENSE +0 -0
  62. {icadkit-0.3.7 → icadkit-0.3.9}/LICENSES/Apache-2.0.txt +0 -0
  63. {icadkit-0.3.7 → icadkit-0.3.9}/LICENSES/PolyForm-Noncommercial-1.0.0.md +0 -0
  64. {icadkit-0.3.7 → icadkit-0.3.9}/LICENSES/parasolid-core-MIT.txt +0 -0
  65. {icadkit-0.3.7 → icadkit-0.3.9}/LICENSES/rust-dependencies.txt +0 -0
  66. {icadkit-0.3.7 → icadkit-0.3.9}/THIRD_PARTY_NOTICES.md +0 -0
  67. {icadkit-0.3.7 → icadkit-0.3.9}/corpus/README.md +0 -0
  68. {icadkit-0.3.7 → icadkit-0.3.9}/corpus/generate.py +0 -0
  69. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/Cargo.toml +0 -0
  70. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/LICENSE +0 -0
  71. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/build.rs +0 -0
  72. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/compression.rs +0 -0
  73. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/document.rs +0 -0
  74. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/error.rs +0 -0
  75. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/header.rs +0 -0
  76. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/limits.rs +0 -0
  77. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/native.rs +0 -0
  78. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/parasolid.rs +0 -0
  79. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/provenance.rs +0 -0
  80. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/record.rs +0 -0
  81. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/resource.rs +0 -0
  82. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/schema.rs +0 -0
  83. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/trailer.rs +0 -0
  84. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/tests/header.rs +0 -0
  85. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/tests/resources.rs +0 -0
  86. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-python/Cargo.toml +0 -0
  87. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-python/LICENSE +0 -0
  88. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-python/src/brep.rs +0 -0
  89. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-python/src/geometry.rs +0 -0
  90. {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-python/src/schema.rs +0 -0
  91. {icadkit-0.3.7 → icadkit-0.3.9}/docs/license.md +0 -0
  92. {icadkit-0.3.7 → icadkit-0.3.9}/docs/parameters.md +0 -0
  93. {icadkit-0.3.7 → icadkit-0.3.9}/docs/references.md +0 -0
  94. {icadkit-0.3.7 → icadkit-0.3.9}/docs/roadmap.md +0 -0
  95. {icadkit-0.3.7 → icadkit-0.3.9}/scripts/check_license.py +0 -0
  96. {icadkit-0.3.7 → icadkit-0.3.9}/scripts/check_public_tree.py +0 -0
  97. {icadkit-0.3.7 → icadkit-0.3.9}/scripts/test_publication.py +0 -0
  98. {icadkit-0.3.7 → icadkit-0.3.9}/scripts/verify_install.py +0 -0
  99. {icadkit-0.3.7 → icadkit-0.3.9}/scripts/verify_native_corpus.py +0 -0
  100. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/__main__.py +0 -0
  101. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_preview_bridge.py +0 -0
  102. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_viewer/index.html +0 -0
  103. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_viewer/viewer.css +0 -0
  104. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_viewer/viewer.js +0 -0
  105. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_viewer_io.py +0 -0
  106. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/api.py +0 -0
  107. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/csg.py +0 -0
  108. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/errors.py +0 -0
  109. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/geometry.py +0 -0
  110. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/native.py +0 -0
  111. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/parameters.py +0 -0
  112. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/preview.py +0 -0
  113. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/py.typed +0 -0
  114. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/references.py +0 -0
  115. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/schema.py +0 -0
  116. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/trailer.py +0 -0
  117. {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/viewer.py +0 -0
  118. {icadkit-0.3.7 → icadkit-0.3.9}/tests/conftest.py +0 -0
  119. {icadkit-0.3.7 → icadkit-0.3.9}/tests/geometry_fixtures.py +0 -0
  120. {icadkit-0.3.7 → icadkit-0.3.9}/tests/parameter_fixtures.py +0 -0
  121. {icadkit-0.3.7 → icadkit-0.3.9}/tests/part_fixtures.py +0 -0
  122. {icadkit-0.3.7 → icadkit-0.3.9}/tests/preview_fixtures.py +0 -0
  123. {icadkit-0.3.7 → icadkit-0.3.9}/tests/saved_fixtures.py +0 -0
  124. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_api.py +0 -0
  125. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_cli.py +0 -0
  126. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_corpus.py +0 -0
  127. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_csg.py +0 -0
  128. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_distribution.py +0 -0
  129. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_extrusion.py +0 -0
  130. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_followup_parts.py +0 -0
  131. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_geometry.py +0 -0
  132. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_inspect.py +0 -0
  133. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_legacy_parts.py +0 -0
  134. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_mirrors.py +0 -0
  135. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_native.py +0 -0
  136. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_native_corpus.py +0 -0
  137. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_native_round.py +0 -0
  138. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_parameters.py +0 -0
  139. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_part_profiles.py +0 -0
  140. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_part_semantics.py +0 -0
  141. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_parts.py +0 -0
  142. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_preview.py +0 -0
  143. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_profile_shapes.py +0 -0
  144. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_references.py +0 -0
  145. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_resource_cli.py +0 -0
  146. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_resources.py +0 -0
  147. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_saved.py +0 -0
  148. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_trailer.py +0 -0
  149. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_v7_parts.py +0 -0
  150. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_v8l4_boundary.py +0 -0
  151. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_version_geometry.py +0 -0
  152. {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_viewer.py +0 -0
  153. {icadkit-0.3.7 → icadkit-0.3.9}/tests/trailer_fixtures.py +0 -0
  154. {icadkit-0.3.7 → icadkit-0.3.9}/tests/viewer_assembly_browser.mjs +0 -0
  155. {icadkit-0.3.7 → icadkit-0.3.9}/tests/viewer_browser.mjs +0 -0
  156. {icadkit-0.3.7 → icadkit-0.3.9}/tests/viewer_mirror_browser.mjs +0 -0
  157. {icadkit-0.3.7 → icadkit-0.3.9}/uv.lock +0 -0
@@ -89,7 +89,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
89
89
 
90
90
  [[package]]
91
91
  name = "icad-core"
92
- version = "0.3.7"
92
+ version = "0.3.9"
93
93
  dependencies = [
94
94
  "flate2",
95
95
  "parasolid-core",
@@ -99,7 +99,7 @@ dependencies = [
99
99
 
100
100
  [[package]]
101
101
  name = "icad-python"
102
- version = "0.3.7"
102
+ version = "0.3.9"
103
103
  dependencies = [
104
104
  "icad-core",
105
105
  "parasolid-core",
@@ -3,7 +3,8 @@ members = ["crates/icad-core", "crates/icad-python"]
3
3
  resolver = "3"
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.7"
6
+ # Release version source. Update with: python3 scripts/bump_version.py X.Y.Z
7
+ version = "0.3.9"
7
8
  edition = "2024"
8
9
  rust-version = "1.88"
9
10
  license = "PolyForm-Noncommercial-1.0.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: icadkit
3
- Version: 0.3.7
3
+ Version: 0.3.9
4
4
  Classifier: Development Status :: 2 - Pre-Alpha
5
5
  Classifier: Programming Language :: Python :: 3 :: Only
6
6
  Classifier: Programming Language :: Python :: Implementation :: CPython
@@ -32,31 +32,17 @@ A Rust-based Python reader for **iCAD SX `.icd` files**. Read saved part
32
32
  structure, coordinate frames, names and selected attributes; inspect native
33
33
  box/cylinder parameters; and extract embedded Parasolid geometry.
34
34
 
35
- Version **0.3.7** adds:
35
+ Supported workflows include:
36
36
 
37
- - [Saved final bodies](docs/saved-bodies.md) on rolling-ball blend surfaces
38
- and on toroidal faces that reach their axis, through `parasolid-core 0.3.7`
39
- and the `preview` extra pinned to `parasolid-kit[occt]==0.3.7`. The local
40
- sample rises from 28,896 to 29,386 of 29,703 bound bodies, with the
41
- previously converted bodies unchanged.
42
- - One declaration per backend version (`Cargo.toml`, `pyproject.toml`); the
43
- runtime checks of the backend version are gone.
44
-
45
- Version 0.3.6 added [saved final bodies](docs/saved-bodies.md) on spun
46
- surfaces (SPUN_SURF), line/arc profile extrusions as [CSG](docs/csg.md)
47
- operands and decoded surface and edge items of the
48
- [trailing container](docs/trailer.md).
49
-
50
- Version 0.3.5 added [view inventories and saved 2D geometry](docs/drawing.md),
51
- twenty-six further catalog-free [schema keys](docs/support.md#schemas), the
52
- [trailing container](docs/trailer.md), saved final bodies bound through V8
53
- association records and V7 source IDs, more [native primitives](docs/native.md),
54
- [saved 3D parameter tables](docs/parameters.md) and explicit
55
- [external-reference resolution](docs/references.md).
56
-
57
- Version 0.3.0 added the [native file viewer](docs/viewer.md), broader
58
- [part access](docs/parts.md), optional [V7L7 boolean evaluation](docs/csg.md)
59
- and [saved final body display](docs/saved-bodies.md).
37
+ - [Part access](docs/parts.md), [saved 3D parameter tables](docs/parameters.md)
38
+ and explicit [external-reference resolution](docs/references.md).
39
+ - [View inventories and saved 2D geometry](docs/drawing.md), plus decoded
40
+ surface and edge items of the [trailing container](docs/trailer.md).
41
+ - The [native file viewer](docs/viewer.md) for qualified
42
+ [native primitives](docs/native.md) and their part properties.
43
+ - Optional [V7L7 boolean evaluation](docs/csg.md) and
44
+ [saved final body display](docs/saved-bodies.md), including qualified spun,
45
+ rolling-ball blend and toroidal surfaces.
60
46
 
61
47
  icadkit is experimental. Unsupported geometry remains in the part inventory;
62
48
  unknown values and source ranges are preserved. Part frames, native primitive
@@ -88,6 +74,33 @@ The optional OCCT dependency requires glibc 2.31+ on Linux; see
88
74
  To build from a source checkout or sdist, install Rust 1.88+ and a C linker,
89
75
  then run `python -m pip install .` (or `'.[preview]'`).
90
76
 
77
+ ## Updating the package version
78
+
79
+ `[workspace.package].version` in `Cargo.toml` is the single release-version
80
+ source. Both Rust crates inherit it, Maturin derives the Python package version
81
+ from it, and `icadkit.__version__` / the CLI use the installed package metadata.
82
+ With Python 3.11+, Cargo and uv available, update it with one command:
83
+
84
+ ```sh
85
+ python3 scripts/bump_version.py X.Y.Z
86
+ python3 scripts/bump_version.py --check --tag vX.Y.Z
87
+ ```
88
+
89
+ Replace `X.Y.Z` with the intended release number. The command updates the
90
+ workspace version, synchronizes `Cargo.lock` and `uv.lock`, and checks their
91
+ consistency. On failure it restores all three files. Add `--offline` to use
92
+ cached dependencies. Stable
93
+ `X.Y.Z` and development `X.Y.Z-dev.N` versions are supported; Python metadata
94
+ uses `X.Y.Z.devN` for the latter. `--check` does not modify the source or locks.
95
+ CI checks the same rules and requires release tags to match the source version.
96
+
97
+ Use `icadkit --version` to check the installed package version and `icadkit info`
98
+ to inspect its backend version. The `parasolid-core` and `parasolid-kit`
99
+ dependency pins have their own versions and are updated separately.
100
+ Usage guides describe capabilities without repeating the package version.
101
+ Record version-specific changes in the [changelog](docs/changelog.md); retain
102
+ the release numbers attached to historical notes and validation results.
103
+
91
104
  ## Part structure, frames and attributes
92
105
 
93
106
  ```python
@@ -120,14 +133,12 @@ spherical, conical and toroidal surfaces. V8L3 resource sharing preserves each
120
133
  occurrence's saved placement. An explicit schema catalog is needed when the
121
134
  exact built-in profile lacks a required type. Counted metadata framing also
122
135
  extends part inventories while preserving unknown records and diagnostics.
123
- Version 0.3.0 also preserves bounded binary attributes as raw owned records,
124
- reads V8L1/V8L2 part inventories without assigning units or geometry, and displays
125
- native cones/frusta and full ring tori. The updated exact V34 profile in
126
- `parasolid-core 0.3.1` removes the catalog requirement for qualified toroidal
127
- resources.
128
-
129
- Version 0.3.5 extends qualified millimetre frames and bounded
130
- native/saved geometry to V7L6/V7L7 and V8L1/V8L2/V8L3, including internal mirrors.
136
+ Bounded binary attributes are preserved as raw owned records. The native viewer
137
+ also displays qualified cones/frusta and full ring tori. The built-in exact V34
138
+ profile covers qualified toroidal resources without an external catalog.
139
+
140
+ Qualified millimetre frames and bounded native/saved geometry cover
141
+ V7L6/V7L7 and V8L1/V8L2/V8L3, including internal mirrors.
131
142
  Signed occurrence orientation is separate from already placed geometry; see
132
143
  [mirror semantics](docs/parts.md#coordinate-frames-and-units). Explicit
133
144
  [external-reference resolution](docs/references.md) now composes qualified child
@@ -4,31 +4,17 @@ A Rust-based Python reader for **iCAD SX `.icd` files**. Read saved part
4
4
  structure, coordinate frames, names and selected attributes; inspect native
5
5
  box/cylinder parameters; and extract embedded Parasolid geometry.
6
6
 
7
- Version **0.3.7** adds:
8
-
9
- - [Saved final bodies](docs/saved-bodies.md) on rolling-ball blend surfaces
10
- and on toroidal faces that reach their axis, through `parasolid-core 0.3.7`
11
- and the `preview` extra pinned to `parasolid-kit[occt]==0.3.7`. The local
12
- sample rises from 28,896 to 29,386 of 29,703 bound bodies, with the
13
- previously converted bodies unchanged.
14
- - One declaration per backend version (`Cargo.toml`, `pyproject.toml`); the
15
- runtime checks of the backend version are gone.
16
-
17
- Version 0.3.6 added [saved final bodies](docs/saved-bodies.md) on spun
18
- surfaces (SPUN_SURF), line/arc profile extrusions as [CSG](docs/csg.md)
19
- operands and decoded surface and edge items of the
20
- [trailing container](docs/trailer.md).
21
-
22
- Version 0.3.5 added [view inventories and saved 2D geometry](docs/drawing.md),
23
- twenty-six further catalog-free [schema keys](docs/support.md#schemas), the
24
- [trailing container](docs/trailer.md), saved final bodies bound through V8
25
- association records and V7 source IDs, more [native primitives](docs/native.md),
26
- [saved 3D parameter tables](docs/parameters.md) and explicit
27
- [external-reference resolution](docs/references.md).
28
-
29
- Version 0.3.0 added the [native file viewer](docs/viewer.md), broader
30
- [part access](docs/parts.md), optional [V7L7 boolean evaluation](docs/csg.md)
31
- and [saved final body display](docs/saved-bodies.md).
7
+ Supported workflows include:
8
+
9
+ - [Part access](docs/parts.md), [saved 3D parameter tables](docs/parameters.md)
10
+ and explicit [external-reference resolution](docs/references.md).
11
+ - [View inventories and saved 2D geometry](docs/drawing.md), plus decoded
12
+ surface and edge items of the [trailing container](docs/trailer.md).
13
+ - The [native file viewer](docs/viewer.md) for qualified
14
+ [native primitives](docs/native.md) and their part properties.
15
+ - Optional [V7L7 boolean evaluation](docs/csg.md) and
16
+ [saved final body display](docs/saved-bodies.md), including qualified spun,
17
+ rolling-ball blend and toroidal surfaces.
32
18
 
33
19
  icadkit is experimental. Unsupported geometry remains in the part inventory;
34
20
  unknown values and source ranges are preserved. Part frames, native primitive
@@ -60,6 +46,33 @@ The optional OCCT dependency requires glibc 2.31+ on Linux; see
60
46
  To build from a source checkout or sdist, install Rust 1.88+ and a C linker,
61
47
  then run `python -m pip install .` (or `'.[preview]'`).
62
48
 
49
+ ## Updating the package version
50
+
51
+ `[workspace.package].version` in `Cargo.toml` is the single release-version
52
+ source. Both Rust crates inherit it, Maturin derives the Python package version
53
+ from it, and `icadkit.__version__` / the CLI use the installed package metadata.
54
+ With Python 3.11+, Cargo and uv available, update it with one command:
55
+
56
+ ```sh
57
+ python3 scripts/bump_version.py X.Y.Z
58
+ python3 scripts/bump_version.py --check --tag vX.Y.Z
59
+ ```
60
+
61
+ Replace `X.Y.Z` with the intended release number. The command updates the
62
+ workspace version, synchronizes `Cargo.lock` and `uv.lock`, and checks their
63
+ consistency. On failure it restores all three files. Add `--offline` to use
64
+ cached dependencies. Stable
65
+ `X.Y.Z` and development `X.Y.Z-dev.N` versions are supported; Python metadata
66
+ uses `X.Y.Z.devN` for the latter. `--check` does not modify the source or locks.
67
+ CI checks the same rules and requires release tags to match the source version.
68
+
69
+ Use `icadkit --version` to check the installed package version and `icadkit info`
70
+ to inspect its backend version. The `parasolid-core` and `parasolid-kit`
71
+ dependency pins have their own versions and are updated separately.
72
+ Usage guides describe capabilities without repeating the package version.
73
+ Record version-specific changes in the [changelog](docs/changelog.md); retain
74
+ the release numbers attached to historical notes and validation results.
75
+
63
76
  ## Part structure, frames and attributes
64
77
 
65
78
  ```python
@@ -92,14 +105,12 @@ spherical, conical and toroidal surfaces. V8L3 resource sharing preserves each
92
105
  occurrence's saved placement. An explicit schema catalog is needed when the
93
106
  exact built-in profile lacks a required type. Counted metadata framing also
94
107
  extends part inventories while preserving unknown records and diagnostics.
95
- Version 0.3.0 also preserves bounded binary attributes as raw owned records,
96
- reads V8L1/V8L2 part inventories without assigning units or geometry, and displays
97
- native cones/frusta and full ring tori. The updated exact V34 profile in
98
- `parasolid-core 0.3.1` removes the catalog requirement for qualified toroidal
99
- resources.
100
-
101
- Version 0.3.5 extends qualified millimetre frames and bounded
102
- native/saved geometry to V7L6/V7L7 and V8L1/V8L2/V8L3, including internal mirrors.
108
+ Bounded binary attributes are preserved as raw owned records. The native viewer
109
+ also displays qualified cones/frusta and full ring tori. The built-in exact V34
110
+ profile covers qualified toroidal resources without an external catalog.
111
+
112
+ Qualified millimetre frames and bounded native/saved geometry cover
113
+ V7L6/V7L7 and V8L1/V8L2/V8L3, including internal mirrors.
103
114
  Signed occurrence orientation is separate from already placed geometry; see
104
115
  [mirror semantics](docs/parts.md#coordinate-frames-and-units). Explicit
105
116
  [external-reference resolution](docs/references.md) now composes qualified child
@@ -1,8 +1,8 @@
1
- {"expected": {"byte_order": "little", "resources": 0}, "family": "synthetic-framing", "holdout": false, "id": "native-uninterpreted", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "native-uninterpreted.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "d4bb2e63c5cccdac66488f8679740b5962713b9d7dbee0e252f584b722958fc7"}
2
- {"expected": {"byte_order": "little", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-little-raw", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-little-raw.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "75102af13dca3d14d69f268b16fc2889242cb23e64365f36b7459c570e9a85bf"}
3
- {"expected": {"byte_order": "little", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-little-zlib", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-little-zlib.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "a2ef56b2ba29e11ecfd838f41e6d87c7cc6587123b4ef7832a45b3bb10a53d2b"}
4
- {"expected": {"byte_order": "big", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-big-raw", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-big-raw.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "68ed994d8917e553b59381eb1d56874c4fc7243080fd0b45cee027066bf7829f"}
5
- {"expected": {"byte_order": "big", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-big-zlib", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-big-zlib.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "c18e05dea1a40493193a7662557663f5b17b8d6204bbe1ce03b3c43ceceaecc9"}
6
- {"expected": {"byte_order": "little", "codes": ["schema.missing_base_schema"], "resources": 1, "status": {"brep": "not_checked", "extraction": "complete", "raw_geometry": "unsupported", "topology": "not_checked"}}, "family": "synthetic-wire", "holdout": false, "id": "unknown-schema", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "unknown-schema.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "18df3e23fa400bc197b9f13c99b280682d92d05bcbbe9bfa0d12fd185996977d"}
7
- {"expected": {"byte_order": "little", "codes": ["brep.invalid_reference"], "resources": 1, "status": {"brep": "invalid", "extraction": "complete", "raw_geometry": "complete", "topology": "not_checked"}}, "family": "synthetic-wire", "holdout": false, "id": "invalid-reference", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "invalid-reference.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "c684eb6d0935eb7693386e8221d08a73378414a2c80ce24deeb8929a533f962d"}
8
- {"expected": {"byte_order": "little", "codes": ["compression.invalid"], "resources": 1, "status": {"brep": "not_checked", "extraction": "invalid", "raw_geometry": "not_checked", "topology": "not_checked"}}, "family": "synthetic-wire", "holdout": false, "id": "invalid-checksum", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "invalid-checksum.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "cc4e1cdf103193a503db080b0e694865bfaff133ef198c7c41460f093c90e426"}
1
+ {"expected": {"byte_order": "little", "resources": 0}, "family": "synthetic-framing", "holdout": false, "id": "native-uninterpreted", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "native-uninterpreted.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "2623d141699e0a846ac1d745b8eb4761421cd6bea4cef7ffb79545919e66f62a"}
2
+ {"expected": {"byte_order": "little", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-little-raw", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-little-raw.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "90049eb96aa3b89c55a9c912efdfcccf41dbf491ccf6f66af84e9acc5185404d"}
3
+ {"expected": {"byte_order": "little", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-little-zlib", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-little-zlib.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "a768f5d63db6bb91f3178c52304002b3dde9eaef7fdd0c0bd5c61160cd94d0ec"}
4
+ {"expected": {"byte_order": "big", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-big-raw", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-big-raw.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "222597e831d61fb1b57f78251155c9cda08419056e258333570f514b58327e33"}
5
+ {"expected": {"byte_order": "big", "codes": [], "resources": 1, "status": {"brep": "complete", "extraction": "complete", "raw_geometry": "complete", "topology": "complete"}, "wire": {"counts": {"bodies": 1, "curves": 1, "edges": 1, "faces": 0, "points": 2, "vertices": 2}, "length": 5}}, "family": "synthetic-wire", "holdout": false, "id": "wire-big-zlib", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "wire-big-zlib.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "9e0aaf42f3d23941c1ecadb354ef026abc4bf9b40979b76a36f71ac996825d93"}
6
+ {"expected": {"byte_order": "little", "codes": ["schema.missing_base_schema"], "resources": 1, "status": {"brep": "not_checked", "extraction": "complete", "raw_geometry": "unsupported", "topology": "not_checked"}}, "family": "synthetic-wire", "holdout": false, "id": "unknown-schema", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "unknown-schema.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "6e6eb075c99ac512366cb0f2dc049cbd5baf52acafd5916f11fbe349ef74793c"}
7
+ {"expected": {"byte_order": "little", "codes": ["brep.invalid_reference"], "resources": 1, "status": {"brep": "invalid", "extraction": "complete", "raw_geometry": "complete", "topology": "not_checked"}}, "family": "synthetic-wire", "holdout": false, "id": "invalid-reference", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "invalid-reference.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "1f796bff73c5c3a536832edaca09718bd3ecedd5645535809806b4e7aaa03f02"}
8
+ {"expected": {"byte_order": "little", "codes": ["compression.invalid"], "resources": 1, "status": {"brep": "not_checked", "extraction": "invalid", "raw_geometry": "not_checked", "topology": "not_checked"}}, "family": "synthetic-wire", "holdout": false, "id": "invalid-checksum", "license": "PolyForm-Noncommercial-1.0.0; see corpus/README.md for profile notices", "path": "invalid-checksum.icd", "provenance": "authored synthetic; corpus/generate.py and tests/geometry_fixtures.py", "sha256": "c4b6299ddd93d618fae769272df983e4f0a20b5d915c853792d7842e0720a0de"}
@@ -0,0 +1,311 @@
1
+ //! Byte-exact model of the directory-indexed container.
2
+ //!
3
+ //! A [`Container`] keeps the MOD record, the DRW word at `+8`, the saved view
4
+ //! names, every indexed record body and the trailing container verbatim. The
5
+ //! framing that depends on them (directory offsets and lengths, `RES`/`V/W`
6
+ //! length words, the MOD total-word field) is recomputed on serialization, so
7
+ //! an unmodified document reproduces its input byte for byte exactly when its
8
+ //! framing follows the observed rules. Nothing is searched for or repaired.
9
+
10
+ use crate::{ByteOrder, Diagnostic, Document, ErrorKind, InspectError};
11
+
12
+ /// Offset of the MOD word that counts the indexed records in 32-bit words.
13
+ const TOTAL_WORDS_AT: usize = 236;
14
+
15
+ /// One directory-indexed record without its opening frame and end tag.
16
+ #[derive(Debug, Clone, PartialEq, Eq)]
17
+ pub struct ContainerRecord {
18
+ /// `RES`, `V/W` or `USR`.
19
+ pub tag: &'static str,
20
+ /// The record's own length word at `+4`. `RES` and `V/W` write their
21
+ /// physical size instead; `USR` writes this word back because old files
22
+ /// store a value that is not the physical size.
23
+ pub declared_words: u32,
24
+ /// Bytes from `+8` (the word shared by every record of one save) up to
25
+ /// the end tag, exclusive. The length is a multiple of four.
26
+ pub body: Vec<u8>,
27
+ }
28
+
29
+ impl ContainerRecord {
30
+ /// Physical size in 32-bit words, including the 12 framing bytes.
31
+ ///
32
+ /// # Errors
33
+ /// Returns `container.alignment` for a body that is not word aligned and
34
+ /// `container.size` for a record that does not fit a 32-bit word count.
35
+ pub fn words(&self) -> Result<u32, InspectError> {
36
+ if !self.body.len().is_multiple_of(4) {
37
+ return Err(InspectError::format(
38
+ ErrorKind::Invalid,
39
+ "container.alignment",
40
+ 0,
41
+ format!(
42
+ "{} body of {} bytes is not word aligned",
43
+ self.tag,
44
+ self.body.len()
45
+ ),
46
+ ));
47
+ }
48
+ u32::try_from(self.body.len() / 4 + 3).map_err(|_| {
49
+ InspectError::format(
50
+ ErrorKind::LimitExceeded,
51
+ "container.size",
52
+ 0,
53
+ format!("{} record exceeds the 32-bit word count", self.tag),
54
+ )
55
+ })
56
+ }
57
+ }
58
+
59
+ /// The indexed container of one document; see the module documentation.
60
+ #[derive(Debug, Clone, PartialEq, Eq)]
61
+ pub struct Container {
62
+ pub byte_order: ByteOrder,
63
+ /// The complete 256-byte MOD record. Bytes 236..240 are replaced by the
64
+ /// recomputed total on serialization; the other fields are copied.
65
+ pub mod_record: [u8; 256],
66
+ /// The DRW word at `+8`, retained verbatim.
67
+ pub drw_word: [u8; 4],
68
+ /// Eight-byte saved view names in directory order, retained verbatim.
69
+ pub view_names: Vec<[u8; 8]>,
70
+ /// `RES`, then one `V/W` per view name, then an optional `USR`.
71
+ pub records: Vec<ContainerRecord>,
72
+ /// Everything after the last indexed record, retained verbatim.
73
+ pub tail: Vec<u8>,
74
+ /// Where the source deviated from the recomputed framing.
75
+ pub diagnostics: Vec<Diagnostic>,
76
+ }
77
+
78
+ fn invalid(code: &'static str, message: impl Into<String>) -> InspectError {
79
+ InspectError::format(ErrorKind::Invalid, code, 0, message)
80
+ }
81
+
82
+ impl Container {
83
+ /// Check the record order, the view-name count and the record sizes.
84
+ fn validate(&self) -> Result<(), InspectError> {
85
+ let mut views = 0;
86
+ for (index, record) in self.records.iter().enumerate() {
87
+ let expected_tag = match (index, record.tag) {
88
+ (0, _) => "RES",
89
+ (_, "USR") if index + 1 == self.records.len() => "USR",
90
+ _ => "V/W",
91
+ };
92
+ if record.tag != expected_tag {
93
+ return Err(invalid(
94
+ "container.record_order",
95
+ format!("record {index} is {}, expected {expected_tag}", record.tag),
96
+ ));
97
+ }
98
+ if record.tag == "V/W" {
99
+ views += 1;
100
+ }
101
+ record.words()?;
102
+ }
103
+ if self.records.first().map(|r| r.tag) != Some("RES") {
104
+ return Err(invalid(
105
+ "container.record_order",
106
+ "the first record must be RES",
107
+ ));
108
+ }
109
+ if views != self.view_names.len() {
110
+ return Err(invalid(
111
+ "container.view_names",
112
+ format!(
113
+ "{views} V/W records but {} view names",
114
+ self.view_names.len()
115
+ ),
116
+ ));
117
+ }
118
+ Ok(())
119
+ }
120
+
121
+ /// Byte length of the DRW record for the current number of views.
122
+ fn drw_bytes(&self) -> usize {
123
+ 32 + 16 * self.view_names.len()
124
+ }
125
+
126
+ /// The MOD total: DRW plus every indexed record, in 32-bit words.
127
+ ///
128
+ /// # Errors
129
+ /// Returns the validation errors of [`Container::to_bytes`].
130
+ pub fn total_words(&self) -> Result<u32, InspectError> {
131
+ self.validate()?;
132
+ let mut total = u32::try_from(self.drw_bytes() / 4)
133
+ .map_err(|_| invalid("container.size", "too many views"))?;
134
+ for record in &self.records {
135
+ total = total
136
+ .checked_add(record.words()?)
137
+ .ok_or_else(|| invalid("container.size", "indexed records exceed 2^32 words"))?;
138
+ }
139
+ Ok(total)
140
+ }
141
+
142
+ /// Serialize with recomputed framing; see the module documentation.
143
+ ///
144
+ /// # Errors
145
+ /// Returns `container.record_order` for records out of order,
146
+ /// `container.view_names` when the view names do not match the `V/W`
147
+ /// records, `container.alignment` for an unaligned body and
148
+ /// `container.size` when a count does not fit its field.
149
+ pub fn to_bytes(&self) -> Result<Vec<u8>, InspectError> {
150
+ let total = self.total_words()?;
151
+ let order = self.byte_order;
152
+ let entries = self.view_names.len() + 2;
153
+ let mut out = Vec::with_capacity(256 + total as usize * 4 + self.tail.len());
154
+ let mut mod_record = self.mod_record;
155
+ mod_record[TOTAL_WORDS_AT..TOTAL_WORDS_AT + 4].copy_from_slice(&order.bytes(total));
156
+ out.extend_from_slice(&mod_record);
157
+ out.extend_from_slice(b"DRW0");
158
+ let drw_words = u32::try_from(self.drw_bytes() / 4)
159
+ .map_err(|_| invalid("container.size", "too many views"))?;
160
+ out.extend_from_slice(&order.bytes(drw_words));
161
+ out.extend_from_slice(&self.drw_word);
162
+ let mut starts = Vec::with_capacity(entries);
163
+ let mut lengths = Vec::with_capacity(entries);
164
+ let mut cursor = u32::try_from(256 + self.drw_bytes())
165
+ .map_err(|_| invalid("container.size", "too many views"))?;
166
+ for record in &self.records {
167
+ let words = record.words()?;
168
+ // Directory starts are byte offsets; lengths are 32-bit words.
169
+ starts.push(cursor);
170
+ lengths.push(words);
171
+ cursor = words
172
+ .checked_mul(4)
173
+ .and_then(|bytes| cursor.checked_add(bytes))
174
+ .ok_or_else(|| invalid("container.size", "indexed records exceed 4 GiB"))?;
175
+ }
176
+ // A missing USR is a zero entry; the directory still reserves it.
177
+ starts.resize(entries, 0);
178
+ lengths.resize(entries, 0);
179
+ for value in starts.iter().chain(&lengths) {
180
+ out.extend_from_slice(&order.bytes(*value));
181
+ }
182
+ for name in &self.view_names {
183
+ out.extend_from_slice(name);
184
+ }
185
+ out.extend_from_slice(b"DRW1");
186
+ for record in &self.records {
187
+ out.extend_from_slice(record.tag.as_bytes());
188
+ out.push(b'0');
189
+ let length = if record.tag == "USR" {
190
+ record.declared_words
191
+ } else {
192
+ record.words()?
193
+ };
194
+ out.extend_from_slice(&order.bytes(length));
195
+ out.extend_from_slice(&record.body);
196
+ out.extend_from_slice(record.tag.as_bytes());
197
+ out.push(b'1');
198
+ }
199
+ out.extend_from_slice(&self.tail);
200
+ Ok(out)
201
+ }
202
+ }
203
+
204
+ impl Document {
205
+ /// Model the indexed container for byte-exact serialization.
206
+ ///
207
+ /// The directory, record ranges and tags were validated when the
208
+ /// document was indexed. A stored MOD total that differs from the
209
+ /// recomputed one is reported as `container.total_words` and is not
210
+ /// copied; the recomputed value is written instead.
211
+ ///
212
+ /// # Errors
213
+ /// Returns `container.range` when an indexed range does not lie inside
214
+ /// the input and `container.view_names` when the directory's view names
215
+ /// do not match its `V/W` records.
216
+ pub fn container(&self) -> Result<Container, InspectError> {
217
+ let data = self.all_bytes();
218
+ let order = self.inspection().header.byte_order;
219
+ let slice = |start: usize, end: usize| {
220
+ data.get(start..end).ok_or_else(|| {
221
+ InspectError::format(
222
+ ErrorKind::Invalid,
223
+ "container.range",
224
+ start as u64,
225
+ format!("indexed range [{start}, {end}) exceeds the input"),
226
+ )
227
+ })
228
+ };
229
+ let indexed = self.records();
230
+ let drw = indexed
231
+ .get(1)
232
+ .filter(|record| record.tag == "DRW")
233
+ .ok_or_else(|| invalid("container.range", "the DRW record is not indexed"))?;
234
+ let drw_len = (drw.byte_range.end - drw.byte_range.start) as usize;
235
+ let views = drw_len.saturating_sub(32) / 16;
236
+ let names_at = 268 + 8 * (views + 2);
237
+ let mut view_names = Vec::with_capacity(views);
238
+ for index in 0..views {
239
+ let mut name = [0; 8];
240
+ name.copy_from_slice(slice(names_at + 8 * index, names_at + 8 * index + 8)?);
241
+ view_names.push(name);
242
+ }
243
+ let mut records = Vec::with_capacity(indexed.len().saturating_sub(2));
244
+ let mut last_end = drw.byte_range.end as usize;
245
+ for record in indexed.iter().skip(2) {
246
+ let start = record.byte_range.start as usize;
247
+ let end = record.byte_range.end as usize;
248
+ let length = slice(start + 4, start + 8)?;
249
+ records.push(ContainerRecord {
250
+ tag: record.tag,
251
+ declared_words: order.u32([length[0], length[1], length[2], length[3]]),
252
+ body: slice(start + 8, end - 4)?.to_vec(),
253
+ });
254
+ last_end = end;
255
+ }
256
+ let mut drw_word = [0; 4];
257
+ drw_word.copy_from_slice(slice(264, 268)?);
258
+ let mut container = Container::new(
259
+ order,
260
+ self.inspection().header.raw_mod,
261
+ drw_word,
262
+ view_names,
263
+ records,
264
+ slice(last_end, data.len())?.to_vec(),
265
+ )?;
266
+ let stored = order.u32([
267
+ container.mod_record[TOTAL_WORDS_AT],
268
+ container.mod_record[TOTAL_WORDS_AT + 1],
269
+ container.mod_record[TOTAL_WORDS_AT + 2],
270
+ container.mod_record[TOTAL_WORDS_AT + 3],
271
+ ]);
272
+ let total = container.total_words()?;
273
+ if stored != total {
274
+ container.diagnostics.push(Diagnostic {
275
+ kind: ErrorKind::Unsupported,
276
+ code: "container.total_words",
277
+ byte_offset: TOTAL_WORDS_AT as u64,
278
+ message: format!("MOD stores {stored} words but the indexed records span {total}"),
279
+ });
280
+ }
281
+ Ok(container)
282
+ }
283
+ }
284
+
285
+ impl Container {
286
+ /// Assemble a container from its retained parts and validate the record
287
+ /// order, the view-name count and the record alignment.
288
+ ///
289
+ /// # Errors
290
+ /// Returns the validation errors of [`Container::to_bytes`].
291
+ pub fn new(
292
+ byte_order: ByteOrder,
293
+ mod_record: [u8; 256],
294
+ drw_word: [u8; 4],
295
+ view_names: Vec<[u8; 8]>,
296
+ records: Vec<ContainerRecord>,
297
+ tail: Vec<u8>,
298
+ ) -> Result<Self, InspectError> {
299
+ let container = Self {
300
+ byte_order,
301
+ mod_record,
302
+ drw_word,
303
+ view_names,
304
+ records,
305
+ tail,
306
+ diagnostics: Vec::new(),
307
+ };
308
+ container.validate()?;
309
+ Ok(container)
310
+ }
311
+ }
@@ -1,6 +1,7 @@
1
1
  //! Bounded iCAD SX framing, structural resource indexing and lazy extraction.
2
2
 
3
3
  mod compression;
4
+ mod container;
4
5
  mod document;
5
6
  mod error;
6
7
  mod header;
@@ -16,6 +17,7 @@ mod schema;
16
17
  mod trailer;
17
18
  mod views;
18
19
 
20
+ pub use container::{Container, ContainerRecord};
19
21
  pub use document::Document;
20
22
  pub use error::{Diagnostic, ErrorKind, InspectError};
21
23
  pub use header::{
@@ -28,8 +30,8 @@ pub use parasolid::{
28
30
  GeometryDiagnostic, GeometryError, GeometryResult, GeometryStatus, SchemaSelection,
29
31
  };
30
32
  pub use parts::{
31
- PartAttributeRecord, PartIndex, PartLimits, PartOpaqueRange, PartProfileInfo, PartRecord,
32
- PartViewRecord,
33
+ PartAttributeRecord, PartIndex, PartLimits, PartMaterialRecord, PartOpaqueRange,
34
+ PartProfileInfo, PartRecord, PartViewRecord,
33
35
  };
34
36
  pub use provenance::{Extraction, SourceRef};
35
37
  pub use reader::ByteOrder;