geometry: face-tagged 3MF follows SolidCraft's namespace and spec (#10)

SolidCraft owns the 3MF face-tag format (its docs/formats/3mf-face-tags.md); the
reader now matches its namespace, and docs/formats/tagged-mesh.md points there while
documenting what ModelCraft's reader does and refuses.


Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

Co-authored-by: jyviko <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-13 15:12:41 -04:00
1 parent 4667655e08
commit 62b0553a89
2 files changed
+46 -98

No files matched your search

+5 -5
View File
@@ -1,8 +1,8 @@
//! Face-tagged meshes in 3MF packages (`docs/formats/tagged-mesh.md`).
//!
//! A 3MF package (3MF Core Specification 1.3) whose triangles may carry `ft:face`, the B-rep face
//! they lie on, in ModelCraft's face-tag namespace. SolidCraft writes it; any other 3MF reads as
//! untagged surfaces (one face each). Reading is plain parsing from bytes (no kernel call, no file
//! they lie on. SolidCraft defines the format and writes it (its `docs/formats/3mf-face-tags.md`);
//! any other 3MF reads as untagged surfaces (one face each). Reading is plain parsing from bytes (no kernel call, no file
//! system), so it also runs in the browser.
//!
//! Packages are untrusted: the file, its parts, zip entries, XML nesting, vertex and triangle
@@ -36,8 +36,8 @@ use modelcraft_model::MAX_COORD;
const MIN_RELATIVE_DET: f64 = 1e-12;
const CORE_NS: &[u8] = b"http://schemas.microsoft.com/3dmanufacturing/core/2015/02";
/// The face-tag namespace, version 1.
pub const FACE_TAGS_NS: &str = "https://github.com/jyviko/modelcraft/formats/face-tags/2026/10";
/// SolidCraft's face-tag namespace, version 1.
pub const FACE_TAGS_NS: &str = "https://github.com/bherbruck/solidcraft/formats/3mf-face-tags/2026/10";
/// One build item of a package: a named surface whose `tri_face` holds the face tags.
#[derive(Debug, Clone, Default, PartialEq)]
@@ -416,7 +416,7 @@ mod tests {
use super::*;
const HEAD: &str = r#"<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02" xmlns:ft="https://github.com/jyviko/modelcraft/formats/face-tags/2026/10"><resources>"#;
const HEAD: &str = r#"<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02" xmlns:ft="https://github.com/bherbruck/solidcraft/formats/3mf-face-tags/2026/10"><resources>"#;
/// A writer built from docs/formats/tagged-mesh.md alone (SolidCraft's is a separate
/// program): shortest round-trip coordinates, one object and build item per surface.
+41 -93
View File
@@ -1,100 +1,48 @@
# Face-tagged mesh exchange (3MF with face tags)
# Face-tagged meshes (3MF with B-rep face tags)
Version 1 (2026-10-13). Namespace `https://github.com/jyviko/modelcraft/formats/face-tags/2026/10`.
Besides opening a `.solidcraft` design, geometry can enter ModelCraft as a triangulated boundary
surface written by another program, with each triangle tagged by the B-rep face it lies on.
ModelCraft reads it into a `SurfaceMesh` (`modelcraft_geometry::tagged_mesh`, command
`geometry.import.mesh`) without involving the kernel.
The second way geometry enters ModelCraft, beside opening a `.solidcraft` design: a triangulated
boundary surface written by another program, each triangle tagged with the B-rep face it lies on.
ModelCraft reads it into a `SurfaceMesh` (`modelcraft_geometry::tagged_mesh`) with no kernel
involved. SolidCraft writes it (`file.export` with `format: "3mf-tagged"`).
## The format is SolidCraft's
## Decision: 3MF Core plus one optional attribute
SolidCraft defines and writes the format (**File → Export 3MF with Face Tags**, `file.export` with
`format: "3mf-tagged"`). The specification is
[`docs/formats/3mf-face-tags.md`](https://github.com/bherbruck/solidcraft/blob/main/docs/formats/3mf-face-tags.md)
in the SolidCraft repository. In short:
The file is an ordinary **3MF package** (3MF Consortium, *3D Manufacturing Format Core
Specification* 1.3, publicly available). The face tags are one attribute in a namespace of our
own, which the Core specification allows on `<triangle>` and `<object>` and which any 3MF consumer
ignores unless the namespace is listed in `requiredextensions` (it is not).
- The file is an ordinary 3MF Core 1.3 package.
- Each `<triangle>` carries `ft:face` (its 0-based B-rep face index), and each `<object>` carries
`ft:faces` (its face count).
- The namespace is `https://github.com/bherbruck/solidcraft/formats/3mf-face-tags/2026/10`
(version 1). It is not listed in `requiredextensions`.
- Coordinates are written in shortest round-trip `f64` form, so they read back exactly.
- Within an object, either every triangle is tagged or none is.
Why not a small binary format of our own (versioned header, f64 positions, u32 triangles, u32
tags):
ModelCraft follows that specification and does not extend it. Because the format is 3MF,
untagged 3MF from any other program goes through the same reader, with one face per object.
- **It is already written and read.** SolidCraft exports and imports 3MF with hostile-input caps;
slicers, viewers and other CAD programs write it too. A tagged file opens in all of them, and
ModelCraft reads untagged 3MF from those programs through the same code ("geometry from other
tools later").
- **One specification to maintain, published by a consortium**, not a byte layout we would have to
version, document and keep two implementations of in step.
- **Nothing is lost.** Coordinates are decimal text; written in shortest round-trip form (Rust's
`{}` for `f64`) every `f64` comes back bit for bit. Triangles and tags are integers.
- The costs (a zip and an XML parser, larger files, slower parsing than raw bytes) are acceptable
for an exchange file. Both libraries (`zip`, `quick-xml`) are MIT, pure Rust, build for wasm and
are the ones SolidCraft already uses.
## What ModelCraft's reader does
## The package
As 3MF Core: a zip holding `[Content_Types].xml`, `_rels/.rels` (a relationship of type
`http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel` to the model part) and the model
part, conventionally `3D/3dmodel.model`.
```xml
<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xml:lang="en-US"
xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:ft="https://github.com/jyviko/modelcraft/formats/face-tags/2026/10">
<metadata name="Application">SolidCraft</metadata>
<resources>
<object id="2" type="model" name="Body1" ft:faces="6">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="10" y="0" z="0"/>
...
</vertices>
<triangles>
<triangle v1="0" v2="2" v3="1" ft:face="0"/>
...
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="2"/>
</build>
</model>
```
### Face tags
| Attribute | On | Value | Meaning |
|---|---|---|---|
| `ft:faces` | `<object>` | integer ≥ 1 | Number of B-rep faces of the body. Optional; when present every `ft:face` of the object is below it. Faces without triangles still count. |
| `ft:face` | `<triangle>` | integer ≥ 0 | The B-rep face (0-based, the body's own face order) the triangle lies on. |
An object's triangles are either **all tagged or none**. A body whose triangles carry no tags reads
as one face (tag 0). A file mixing tagged and untagged triangles in one object is rejected.
### Writer rules (version 1)
- `unit="millimeter"`, Z up.
- One `<object type="model">` with a `<mesh>` per body, named after the body, one `<item>` per
object, no `transform`, no components.
- Vertices shared within an object (coincident positions merged), triangles counter-clockwise
seen from outside, no degenerate triangles; coordinates in shortest round-trip decimal form.
- Colours, if written, use 3MF's own `basematerials` (`pid`/`p1`); they are independent of the tags.
- Deterministic bytes: fixed part order (`[Content_Types].xml`, `_rels/.rels`, the model),
fixed zip timestamps, no hash-ordered output.
### Reader rules (ModelCraft)
- Every build item becomes one `SurfaceMesh`, in millimetres (`unit` converted), named after its
object (`Mesh<n>` when unnamed). An item's `transform` is applied; a mirroring transform reverses
the winding; a singular transform (one that flattens the object) is refused. Components are
not read yet (an error that says so).
- Untrusted input: file ≤ 512 MB, a part ≤ 1 GB uncompressed, ≤ 10 000 zip entries, ≤ 20 million
vertices and 20 million triangles in total, XML nesting ≤ 256. Coordinates must be finite with
magnitude ≤ 10⁹ mm (the FE model's limit), in the file and once placed; vertex indices must exist; tags must be below `ft:faces` when it is given.
Any violation is an error naming the problem; nothing panics.
## Versions
A later version gets a new namespace (`…/face-tags/<year>/<month>`); readers accept the versions
they know and treat an unknown face-tag namespace as untagged.
- **Surfaces.** Every build item becomes one `TaggedSurface`: a `SurfaceMesh` with its name and
face count. Lengths are converted to millimetres from `unit`. The surface is named after its
object, or `Mesh<n>` when the object is unnamed.
- **Transforms.** An item's `transform` is applied, and a mirroring transform reverses the
winding. A singular transform (one that flattens the object) is refused. Components are not read
yet; the reader returns an error that says so.
- **Tags.**
- Tags are matched by namespace URI, not by prefix.
- An untagged object becomes one face (tag 0), and an unknown face-tag namespace reads as
untagged.
- An object that mixes tagged and untagged triangles is rejected, as is any tag not below
`ft:faces`.
- **Limits.** All input is treated as untrusted:
- The file is at most 512 MB and must be a regular file, read through the virtual file system.
- Each part is at most 1 GB uncompressed, in at most 10 000 zip entries.
- At most 20 million vertices and 20 million triangles, counted over every placement.
- At most 10 000 build items, and XML nesting at most 256 deep.
- Coordinates must be finite and within ±10⁹ mm (`modelcraft_model::MAX_COORD`), both as read
and once placed.
- Every vertex index must exist.
- **Errors.** Any violation is an error that names the problem. Nothing panics.