Skip to content

An imported model's materials are reported, and its images are written where you say - #335

Merged
CagdasErturk merged 6 commits into
mainfrom
import-gltf-tool
Sep 9, 2026
Merged

An imported model's materials are reported, and its images are written where you say#335
CagdasErturk merged 6 commits into
mainfrom
import-gltf-tool

Conversation

@CagdasErturk

Copy link
Copy Markdown
Member

asset-import read geometry and discarded everything else a glTF document said about itself. It reports the materials now, and writes the images into a directory the caller names.

--images <path> is the asking. Without it nothing is written and everything is still reported, so a caller learns a model carries textures before deciding where they go. A command that scattered them beside the blob because the input happened to hold some would be writing files nobody asked for. The directory is created, because the flag names a destination for a set of files whose size the caller cannot know in advance — unlike --out, which names one file whose parent they already chose.

Named by index, not by the document's name for them. Each file is image-<n>, after the document's own address for the image. A name inside a file is not a thing that should decide where bytes land on disk, and the index is what a material's texture reference points at — so the JSON and the directory can be matched without guessing.

The extension is the one judgement this arm makes about a media type, and it is a judgement about a name. Nothing here decodes anything; a type it cannot name may be perfectly readable by something else, and calling a JPEG .png would be inventing a fact the document never stated. image/png and image/jpeg are the two the format's own schema names. Anything else, or nothing at all, is UnknownMediaType — and only ever when --images asked for a file to be named. Without the flag the type is still just reported, which is what the layer below promises.

gltf::tables is new beside gltf::read, doing the same container dispatch for the material and image tables that read does for geometry. Two callers already had that dispatch written out and each had it subtly different; a third copy inside the tool was not worth having. Its images own their bytes, which a borrowed form could not — the parsed document lives inside the call and nothing pointing into it can be handed back. A caller wanting to avoid that copy holds the parse itself and calls gltf::images.

The two commands still compose and neither learns about the other: import writes files, pack collects them. No pack is written here, and no image is decoded here or below.

A format that carries no images answers with two empty tables rather than a refusal. Asking an STL for its textures is a fair question with a true answer.

Also corrects the mesh crate's own description of how an image's media type is chosen: it still described a comparison that no longer happens.

Local gates: cargo fmt --all --check clean, cargo clippy --workspace --all-targets -D warnings zero errors and zero warnings, cargo test --workspace 264 suites with zero failures, and cargo test -p renew-cli --test cli 73 passed including five new integration tests.

…are written where you say

`asset-import` read geometry and threw away everything else a glTF
document said about itself. It reports the materials now, and writes the
images into a directory the caller names.

`--images <path>` is the asking. Without it nothing is written and
everything is still reported, so a caller learns a model carries textures
before deciding where they go -- a command that scattered them beside the
blob because the input happened to hold some would be writing files
nobody asked for. The directory is created, because the flag names a
destination for a set of files whose size the caller cannot know in
advance; `--out` names one file whose parent they already chose.

Each image is written as `image-<n>` after the document's own address for
it, not after the name the document gave it: a name in a file is not a
thing that should decide where bytes land on disk, and the index is what
a material's texture reference points at, so the JSON and the directory
can be matched without guessing.

The extension comes from the media type, and that is the one judgement
this arm makes about one. It is a judgement about a *name*: nothing here
decodes anything, a type it cannot name may be perfectly readable by
something else, and calling a JPEG `.png` would be inventing a fact the
document never stated. `image/png` and `image/jpeg` are the two the
format's own schema names; anything else, or nothing at all, is
`UnknownMediaType` -- and only ever when `--images` asked for a file to
be named. Without the flag the type is still just reported.

`gltf::tables` is new beside `gltf::read`: it does the same container
dispatch for the material and image tables that `read` does for geometry.
Two callers had that dispatch written out already and each had it subtly
different; a third copy in the tool was not worth having. Its images own
their bytes, which a borrowed form could not -- the parsed document lives
inside the call and nothing pointing into it can be handed back.

The two commands still compose and neither learns about the other:
import writes files, pack collects them. No pack is written here, and no
image is decoded here or below.

A format that carries no images answers with two empty tables rather than
a refusal. Asking an STL for its textures is a fair question with a true
answer.

Also corrects the mesh crate's own description of how an image's media
type is chosen, which still described a comparison that no longer
happens.
…under them

Six sites the new import code reached and nothing executed, each given a
test rather than an exemption: the two filesystem failures (`--images`
pointed at an existing file, so the directory cannot be made; and at a
directory already holding `image-0.png`, so the file cannot be written),
the half of `UnknownMediaType` that fires when a document names no type
at all, the arm that carries a table's refusal out of an import whose
geometry read perfectly, the `OPAQUE` and `BLEND` alpha modes -- the
first fixture only used `MASK` -- and the whole human-readable output
arm, which is a second piece of code that would otherwise ship to
everyone who does not pass `--json` having never run once.

The one exempt block in this file is the determinism emit half, and the
import grew the code above it, so all fifty-two of its lines moved by
exactly 313. Checked rather than assumed: every old position maps to a
reported-uncovered position with that same delta, and nothing else in
the block changed.
…odel

The import read a glTF's material and image tables whether or not
`--images` asked for them, and the image reader refuses a `uri` naming a
second file. So a document with embedded geometry and ordinary sibling
`.png` textures -- the commonest shape a real glTF has -- stopped
importing, for a reason that says nothing about whether its geometry is
sound.

Reading a model is what the command is for. A table that will not read is
reported now: `materials`, `textures` and `images` come back null with
`tables_refusal` carrying the refusal's name and its sentence, and the
blob is written. It stays fatal when `--images` was given, because then
the caller asked for the thing that cannot be delivered.

Nothing is written until every image's name is settled. The loop checked
one image and then wrote it, so a document whose third image had a media
type this cannot name left the first two on disk beside a blob -- the
half-written directory the comment beside it claimed was impossible.

A material names a texture and a texture names an image, which is a step
the report skipped: a caller pairing a material to a file by index was
wrong whenever a texture's index was not its image's. `gltf::textures`
reads that table and the envelope carries it. The material report also
dropped a normal map's scale and an occlusion map's strength -- the two
values a default cannot recover once a document has stated them --
while the doc above it argued that nothing is dropped.

`--images ""` named the working directory, and `create_dir_all("")`
succeeds, so it scattered a model's textures wherever the process
happened to be standing. Refused where the other path rules live.

`Format::tables` sits beside `Format::read` and is exhaustive, so the
tool no longer matches on the format enum behind a wildcard and a format
added later has to answer the question before it compiles. Its `None`
means "this format does not state these in this vocabulary", which is
why the fields are null rather than empty for an OBJ: Wavefront's
material model is a different thing, and an empty array would be saying
something false about the file.

`ExternalResource` said a document kept its *geometry* somewhere else,
which was true when only a buffer could raise it. An image raises it too.

The corpus image floor parsed each seed as JSON directly, so every
binary glTF in it was skipped silently and no container seed ever
reached the image table.
…k follows the code

`gltf::textures` reads which image each texture draws from, and shipped
without a test of its own. Four now: a source that is named, a texture
that names none -- `None` rather than zero, because the format leaves
`source` optional for an extension to supply and defaulting it would
point every such texture at whichever image happened to be first -- an
absent table, and the two wrong-kind refusals.

The unreadable-tables message had a test that passed `--json`, so its
human-readable half had never run. Two output modes are two pieces of
code.

The one exempt block in the command's source is the determinism emit
half, and the code above it moved again, so its fifty-two lines shift by
ninety-five. Checked rather than assumed: each recorded line was located
by its own text and every one is byte-identical at that offset.
Reading a document's tables copied every image out of it. An image
stored in a buffer view borrows the document, so owning it copies -- and
nothing in the format says two images must name two views. A document
that points a thousand of them at one shared megabyte costs about thirty
bytes an entry to write and a gigabyte to hold. Measured: 375 KB in,
1,099 MB allocated, and the curve is quadratic in the input's length, so
an eight-megabyte document reaches hundreds of gigabytes. The command
took 437 ms and a gigabyte over an import that used to take 5 ms and
three megabytes, and exited successfully.

This crate already had the rule: amplification is the danger, not
allocation. Geometry answers to a ceiling and so does the material
table. Images now answer to the same one, counted in bytes rather than
entries because one image can be the whole ceiling on its own and a
thousand tiny ones are harmless.

The copy is also asked for rather than assumed. It was being paid by
callers who only wanted to know what a model carries -- on a
four-megabyte texture it is 99% of the call's time and 99.9% of its
bytes, and the import paid it whether or not any file was going to be
written. `ImageBytes` states the choice at the call site, and the import
asks to count unless it was told where to put the files.

`Stored` replaces the owning half of `Image`: it states its length
separately from its bytes, so a report never needs the bytes in order to
say how big something is.

Two tests could not fail. Both asserted that a refusal's text mentions
`--images`, and every parse error prints the usage block, which lists
`--images` -- so they held however the guard behaved. They assert the
sentence the rule produces now.

And the material and image report is compared by value. Every number in
it could have been dropped, negated or swapped without a test noticing:
metallic read from roughness, an image's length reported as zero, every
texture role relabelled, the texture index replaced with a constant.
…ted at the boundary

Nothing called `gltf::tables`. The target read the borrowing half and
wrote its own container dispatch, so the one path that copies an image
was never attacked -- which is how a document that points many images at
one buffer view got as far as it did. It is called now, from the top so
that a document whose container refuses in the borrowing half still
reaches it, and the two modes are held against each other: the same
materials, textures and count, and per image the same name, type and
length, with an image that kept bytes keeping exactly as many as it
measured. Counting is asserted to hold no bytes at all, which is the
property the distinction exists for.

A seed for the shape, since nothing else in the corpus reaches that
arithmetic: four images over one view.

The image ceiling gets the boundary test its siblings have. The copy
that exactly fills it is allowed, one byte past is refused by name, and
a sum that saturates is over the ceiling rather than back under it --
lengths come out of the document, and two near the pointer's width would
otherwise wrap to an acceptance.

The writer's arm for an image whose bytes were measured rather than kept
is gone. The caller asks for bytes and files together, so nothing could
reach it, and an arm nothing can run is worse than no arm: it reads as a
case that has been handled. The pass builds the names and the bytes
together instead.

The exempt block in the command's source moved again with the code above
it. Located by content and verified line by line at the new offset.
@CagdasErturk
CagdasErturk merged commit 510d7af into main Sep 9, 2026
31 checks passed
@CagdasErturk
CagdasErturk deleted the import-gltf-tool branch September 9, 2026 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant