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.
- {icadkit-0.3.7 → icadkit-0.3.9}/Cargo.lock +2 -2
- {icadkit-0.3.7 → icadkit-0.3.9}/Cargo.toml +2 -1
- {icadkit-0.3.7 → icadkit-0.3.9}/PKG-INFO +44 -33
- {icadkit-0.3.7 → icadkit-0.3.9}/README.md +44 -33
- {icadkit-0.3.7 → icadkit-0.3.9}/corpus/public.jsonl +8 -8
- icadkit-0.3.9/crates/icad-core/src/container.rs +311 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/lib.rs +4 -2
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/parts.rs +51 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/reader.rs +14 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/views.rs +92 -0
- icadkit-0.3.9/crates/icad-core/tests/container.rs +203 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-python/src/document.rs +129 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-python/src/lib.rs +1 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/docs/api.md +72 -12
- {icadkit-0.3.7 → icadkit-0.3.9}/docs/changelog.md +116 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/docs/csg.md +3 -3
- icadkit-0.3.9/docs/drawing.md +242 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/docs/native.md +9 -9
- {icadkit-0.3.7 → icadkit-0.3.9}/docs/parts.md +22 -3
- {icadkit-0.3.7 → icadkit-0.3.9}/docs/preview.md +3 -3
- {icadkit-0.3.7 → icadkit-0.3.9}/docs/saved-bodies.md +16 -16
- icadkit-0.3.9/docs/shapes.md +103 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/docs/support.md +36 -19
- {icadkit-0.3.7 → icadkit-0.3.9}/docs/trailer.md +3 -4
- {icadkit-0.3.7 → icadkit-0.3.9}/docs/viewer.md +4 -4
- icadkit-0.3.9/docs/writing.md +189 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/pyproject.toml +2 -0
- icadkit-0.3.9/scripts/bump_version.py +132 -0
- icadkit-0.3.9/scripts/test_versions.py +229 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/scripts/verify_artifacts.py +9 -7
- {icadkit-0.3.7 → icadkit-0.3.9}/scripts/verify_corpus.py +1 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/__init__.py +58 -1
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_core.pyi +27 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_csg_occt.py +183 -39
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_saved_occt.py +28 -21
- icadkit-0.3.9/src/icadkit/_shape_occt.py +71 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/cli.py +2 -0
- icadkit-0.3.9/src/icadkit/container.py +137 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/document.py +20 -0
- icadkit-0.3.9/src/icadkit/drawing.py +1104 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/models.py +18 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/parts.py +101 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/saved.py +40 -27
- icadkit-0.3.9/src/icadkit/shape.py +693 -0
- icadkit-0.3.9/src/icadkit/versions.py +74 -0
- icadkit-0.3.9/src/icadkit/views.py +310 -0
- icadkit-0.3.9/src/icadkit/write.py +1750 -0
- icadkit-0.3.9/tests/drawing_fixtures.py +203 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/fixture_builders.py +2 -0
- icadkit-0.3.9/tests/test_container.py +77 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_drawing.py +266 -3
- icadkit-0.3.9/tests/test_parts_material.py +116 -0
- icadkit-0.3.9/tests/test_shape.py +376 -0
- icadkit-0.3.9/tests/test_views_placement.py +155 -0
- icadkit-0.3.9/tests/test_write.py +722 -0
- icadkit-0.3.7/docs/drawing.md +0 -131
- icadkit-0.3.7/src/icadkit/drawing.py +0 -304
- icadkit-0.3.7/src/icadkit/views.py +0 -114
- icadkit-0.3.7/tests/drawing_fixtures.py +0 -94
- {icadkit-0.3.7 → icadkit-0.3.9}/COMMERCIAL-LICENSE.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/LICENSE +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/LICENSES/Apache-2.0.txt +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/LICENSES/PolyForm-Noncommercial-1.0.0.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/LICENSES/parasolid-core-MIT.txt +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/LICENSES/rust-dependencies.txt +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/THIRD_PARTY_NOTICES.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/corpus/README.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/corpus/generate.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/Cargo.toml +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/LICENSE +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/build.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/compression.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/document.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/error.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/header.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/limits.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/native.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/parasolid.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/provenance.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/record.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/resource.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/schema.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/src/trailer.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/tests/header.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-core/tests/resources.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-python/Cargo.toml +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-python/LICENSE +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-python/src/brep.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-python/src/geometry.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/crates/icad-python/src/schema.rs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/docs/license.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/docs/parameters.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/docs/references.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/docs/roadmap.md +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/scripts/check_license.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/scripts/check_public_tree.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/scripts/test_publication.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/scripts/verify_install.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/scripts/verify_native_corpus.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/__main__.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_preview_bridge.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_viewer/index.html +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_viewer/viewer.css +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_viewer/viewer.js +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/_viewer_io.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/api.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/csg.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/errors.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/geometry.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/native.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/parameters.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/preview.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/py.typed +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/references.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/schema.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/trailer.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/src/icadkit/viewer.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/conftest.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/geometry_fixtures.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/parameter_fixtures.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/part_fixtures.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/preview_fixtures.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/saved_fixtures.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_api.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_cli.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_corpus.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_csg.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_distribution.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_extrusion.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_followup_parts.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_geometry.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_inspect.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_legacy_parts.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_mirrors.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_native.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_native_corpus.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_native_round.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_parameters.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_part_profiles.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_part_semantics.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_parts.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_preview.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_profile_shapes.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_references.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_resource_cli.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_resources.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_saved.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_trailer.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_v7_parts.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_v8l4_boundary.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_version_geometry.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/test_viewer.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/trailer_fixtures.py +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/viewer_assembly_browser.mjs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/viewer_browser.mjs +0 -0
- {icadkit-0.3.7 → icadkit-0.3.9}/tests/viewer_mirror_browser.mjs +0 -0
- {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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
35
|
+
Supported workflows include:
|
|
36
36
|
|
|
37
|
-
- [
|
|
38
|
-
and
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
- [
|
|
10
|
-
and
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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,
|
|
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;
|