Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ website/node_modules/
docs/*
!docs/AUTO_UPDATER_DESIGN.md
!docs/IMAGE_TO_3D_SPIKE_764.md
!docs/IMAGE_TO_3D_QUALITY.md
!docs/MESH_SEGMENTATION_STRATEGY.md

# minisign — never commit secret keys
Expand Down
6 changes: 5 additions & 1 deletion CLAUDE.md

Large diffs are not rendered by default.

94 changes: 94 additions & 0 deletions docs/IMAGE_TO_3D_QUALITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Image-to-3D quality roadmap (post-#764)

Goal: close the visible gap between our local TripoSR output and commercial
services (Meshy, Tripo3D) while staying permissive-license-clean and local.

## Shipped: the quality pass (this branch)

Three classical post-processing stages, ON by default, between marching cubes
and mesh construction (`MeshGenPredictor::Options {smoothMesh, refineSurface,
bakeTexture, textureSize}`):

1. **Taubin smoothing** (`MeshRefine::taubinSmooth`) — λ|μ alternating Laplacian
(λ=0.5, μ=-0.53, 6 iterations). Removes the res³-grid stair-stepping that
reads as "AI blob" without the volume shrinkage of plain Laplacian.
2. **Iso-surface reprojection** (`MeshRefine::isoProjectStep`) — one Newton step
per vertex toward the decoder's true zero level set, using forward-difference
gradients from 4 extra decoder probes per vertex (ε = half a grid cell, step
clamped to one cell). Restores detail the MC grid quantized away and undoes
residual smoothing drift. Cost: nv×4 extra decoder queries (~1 grid chunk).
3. **Diffuse texture bake** (`MeshGenBaker`) — xatlas auto-unwrap, UV-space
triangle rasterization (barycentric texel → 3D surface point), per-texel
decoder colour queries, chart-border dilation. Output: UV0 + a real texture
(default 1024², atlas may grow to fit). Replaces per-vertex colour, whose
effective resolution was capped by MC vertex density AND which didn't
survive export to most viewers (rendered flat white). The baked texture
survives every export path (`MeshGenBuilder` saves the PNG + registers a
resource location; the CLI lands it next to the output mesh).

Verified end-to-end on macOS: baseline (no pass) renders as a white unlit blob
through the glb → turntable round-trip; the quality output renders textured and
smooth. ~30 s total at res 192 + 1024² bake on an M-series laptop (338% CPU).

Opt-outs: CLI `--no-smooth --no-refine --no-bake-texture --texture-size N`,
MCP `smooth/refine/bake_texture/texture_size`, GUI inherits defaults.

## Next: candidate upgrades (researched 2026-07, licenses verified)

Ranked by (quality gain × feasibility ÷ license risk). Full license audit in
the table below.

### 1. TripoSG as a second, higher-quality geometry backend (RECOMMENDED)
- VAST-AI `TripoSG` (SIGGRAPH 2025): **MIT code + MIT weights** (verified HF
card + repo LICENSE) — the only 2025-class quality jump that is fully
license-clean end-to-end. Same org as UniRig (#408).
- Rectified-flow DiT (1.5B) + SDF VAE. Architecture maps onto our existing
ONNX pattern: DINOv2 image conditioning (export as one graph), the DiT step
as a second graph driven by a C++ flow loop (we already hand-roll UniRig's
autoregressive decode), and a query-points→SDF cross-attention decoder that
is nearly identical to our chunked TripoSR decode. Our MC already handles
SDF fields (inside-positive, iso 0).
- Reported quality ≈ commercial Tripo 2.0 (Normal-FID 5.81 vs ~20 for
TripoSR-class LRMs). Geometry only → reuse this branch's bake for colour...
but TripoSG has no colour decoder, so texture comes from input-image
projection (see 3) or a texture model.
- Cost: ~3 GB fp16 download (int8-quantize like the TripoSR tiers), minutes of
CPU inference (expose a steps knob; rectified flow tolerates few steps).
No community ONNX export exists — `scripts/export-triposg-onnx.py` is an
export effort comparable to the UniRig one (3 graphs + flow loop).
- UX: `--backend triposr|triposg` (keep TripoSR as the fast/preview tier),
mirroring the fp32/int8 tier picker.

### 2. Cheap wins still on the table for the TripoSR path
- **Real-ESRGAN upscale of the baked texture** (infra already shipped, #405):
bake at 1024 → upscale 2× → sharper texture for ~8 s extra. One flag + one
call into `TextureUpscaler`.
- **Input-image front-projection blend**: for texels whose surface normal faces
the input camera, blend the actual input pixels over the decoder colour
(weight = normal·view). Recovers photo-crisp detail on the front. Needs the
predictor-space camera convention calibrated once (render a known mesh,
compare projections; TripoSR's training camera is not exported with the
ONNX graphs).
- **Higher default MC resolution for final exports** (256 → 320/384) now that
smoothing+reprojection hide grid artifacts; scale texel density with it.

### 3. Rejected / parked (license or feasibility)

| Model | Verdict |
|---|---|
| TRELLIS / TRELLIS.2-4B (MS, MIT) | Best open quality but 4B CPU-hostile, sparse attention has no clean ONNX path, and the texture stage depends on **nvdiffrast (NVIDIA non-commercial)**. Park until someone strips the NC deps (Hi3DGen proved it's possible for geometry). |
| Hunyuan3D-2.x (Tencent) | Community license **excludes EU/UK/South Korea** + MAU cap → fails redistribution bar. |
| SPAR3D / SF3D (Stability) | Community license revenue cap → rejected (same as #764 spike). |
| InstantMesh (Apache) | Pipeline requires Zero123++ weights (**CC-BY-NC**) → rejected. |
| MeshAnything, CraftsMan | NC / AGPL-tainted weights → rejected. |
| Fine-tune TripoSG on CC-subset Objaverse (~740K permissive objects) | Feasible (PartCrafter fine-tuned the DiT only) but weeks of GPU work; only worth it for domain specialization later. |

Comment thread
fernandotonon marked this conversation as resolved.
## What Meshy/Tripo actually do differently
Multi-stage native-3D pipelines: big geometry diffusion (quad topology, part
segmentation) + a separate multi-view **PBR texture diffusion** stage
(albedo/normal/roughness/metallic) + retopo/UV post-stages. Open models match
the geometry of roughly one commercial generation back (TripoSG ≈ Tripo 2.0);
the durable gaps are texture/PBR quality and quad retopo — and the project
already has PBRify map synthesis (#404) and QuadRetopo (#401) to chain onto
generated meshes (`qtmesh material --texture gen_diffuse.png --generate-pbr`,
`qtmesh retopo`).
241 changes: 191 additions & 50 deletions qml/PropertiesPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -1498,10 +1498,18 @@ Rectangle {
id: meshGenToolsComponent

Column {
id: mgRoot
width: parent ? parent.width : 200
padding: 8
spacing: 6

// Per-step progress state (see the step list below). `mgSteps` is
// built from the enabled checkboxes when Generate is clicked, so
// the list mirrors exactly the stages that will run.
property var mgSteps: []
property int mgActiveIdx: -1
property real mgActiveProgress: -1 // 0..1; < 0 → indeterminate

// A small local button factory (raw QML — the Themed* wrappers blank
// this dynamically-loaded panel, so we style raw controls with the
// PropertiesPanelController palette to match the Inspector).
Expand Down Expand Up @@ -1545,6 +1553,45 @@ Rectangle {
}
}

// Inspector-styled CheckBox (flat 16px box + checkmark, palette
// colors) — one factory for the pipeline-stage toggles below,
// matching the ThemedCheckBox look without the wrapper that breaks
// this dynamically-loaded panel.
component InspectorCheck: CheckBox {
id: icRoot
spacing: 6
enabled: !MeshGenController.busy
indicator: Rectangle {
x: icRoot.leftPadding
y: icRoot.height / 2 - height / 2
implicitWidth: 16
implicitHeight: 16
radius: 2
color: icRoot.checked
? PropertiesPanelController.highlightColor
: PropertiesPanelController.inputColor
border.color: PropertiesPanelController.borderColor
border.width: 1
opacity: icRoot.enabled ? 1.0 : 0.45
Comment on lines +1564 to +1575

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restore a visible keyboard focus state.

The custom indicator always uses the same border, so tabbing through these new checkboxes gives no visible focus ring after replacing the native CheckBox visuals.

Proposed fix
-                    border.color: PropertiesPanelController.borderColor
-                    border.width: 1
+                    border.color: icRoot.visualFocus
+                        ? PropertiesPanelController.highlightColor
+                        : PropertiesPanelController.borderColor
+                    border.width: icRoot.visualFocus ? 2 : 1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
indicator: Rectangle {
x: icRoot.leftPadding
y: icRoot.height / 2 - height / 2
implicitWidth: 16
implicitHeight: 16
radius: 2
color: icRoot.checked
? PropertiesPanelController.highlightColor
: PropertiesPanelController.inputColor
border.color: PropertiesPanelController.borderColor
border.width: 1
opacity: icRoot.enabled ? 1.0 : 0.45
indicator: Rectangle {
x: icRoot.leftPadding
y: icRoot.height / 2 - height / 2
implicitWidth: 16
implicitHeight: 16
radius: 2
color: icRoot.checked
? PropertiesPanelController.highlightColor
: PropertiesPanelController.inputColor
border.color: icRoot.visualFocus
? PropertiesPanelController.highlightColor
: PropertiesPanelController.borderColor
border.width: icRoot.visualFocus ? 2 : 1
opacity: icRoot.enabled ? 1.0 : 0.45
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qml/PropertiesPanel.qml` around lines 1556 - 1567, The custom CheckBox
indicator in PropertiesPanel.qml is missing a visible keyboard focus state, so
tab navigation provides no focus ring. Update the CheckBox/indicator styling
around the indicator Rectangle to react to focus (or activeFocus) and draw a
distinct focus outline or border when focused, while keeping the current
checked/unchecked appearance otherwise. Use the existing
PropertiesPanelController colors and the indicator Rectangle/CheckBox component
as the place to add the focus-visible behavior.

Text {
anchors.centerIn: parent
visible: icRoot.checked
text: "✓"
color: PropertiesPanelController.textColor
font.pixelSize: 12
font.bold: true
}
}
contentItem: Text {
text: icRoot.text
color: PropertiesPanelController.textColor
font.pixelSize: 11
leftPadding: icRoot.indicator.width + icRoot.spacing
verticalAlignment: Text.AlignVCenter
opacity: icRoot.enabled ? 1.0 : 0.45
}
}

// Inspector-styled ComboBox (raw ComboBox re-skinned with the
// PropertiesPanelController palette — same look as ThemedComboBox, but
// inlined because the Themed* wrappers blank this dynamically-loaded
Expand Down Expand Up @@ -1706,63 +1753,148 @@ Rectangle {
}
}

// Remove-background toggle — styled to match the Inspector (flat 16px
// box + checkmark, PropertiesPanelController palette), matching the
// ThemedCheckBox look without the wrapper that breaks this panel.
CheckBox {
// ---- User-selectable pipeline stages -------------------------------
// Each toggle maps 1:1 to a stage of the generation pipeline (see
// MeshGenController::generateSelected). Defaults = the polished
// pipeline: smooth + refine + bake + PBR maps; upscale opt-in.
InspectorCheck {
id: mgRemoveBg
text: "Remove background"
checked: true
enabled: !MeshGenController.busy
spacing: 6
indicator: Rectangle {
x: mgRemoveBg.leftPadding
y: mgRemoveBg.height / 2 - height / 2
implicitWidth: 16
implicitHeight: 16
radius: 2
color: mgRemoveBg.checked
? PropertiesPanelController.highlightColor
: PropertiesPanelController.inputColor
border.color: PropertiesPanelController.borderColor
border.width: 1
opacity: mgRemoveBg.enabled ? 1.0 : 0.45
Text {
anchors.centerIn: parent
visible: mgRemoveBg.checked
text: "✓"
color: PropertiesPanelController.textColor
font.pixelSize: 12
font.bold: true
}
}
contentItem: Text {
text: mgRemoveBg.text
color: PropertiesPanelController.textColor
font.pixelSize: 11
leftPadding: mgRemoveBg.indicator.width + mgRemoveBg.spacing
verticalAlignment: Text.AlignVCenter
}
}
InspectorCheck {
id: mgSmooth
text: "Smooth mesh (Taubin)"
checked: true
}
InspectorCheck {
id: mgRefine
text: "Refine surface (re-project)"
checked: true
}
InspectorCheck {
id: mgBake
text: "Bake diffuse texture"
checked: true
}
InspectorCheck {
id: mgPbr
text: "Generate PBR maps (normal + roughness)"
checked: true
enabled: !MeshGenController.busy && mgBake.checked
}
InspectorCheck {
id: mgUpscale
text: "Upscale texture 2× (Real-ESRGAN)"
checked: false
enabled: !MeshGenController.busy && mgBake.checked
}

// Step 2: generate from the selected image. Disabled until one is
// picked (or while busy).
// picked (or while busy). Builds the per-step progress list from
// the enabled stages before kicking off.
InspectorButton {
text: "Generate 3D"
clickEnabled: !MeshGenController.busy
&& MeshGenController.selectedImagePath.length > 0
onClicked: MeshGenController.generateSelected(
mgResCombo.resValue, mgRemoveBg.checked, mgQualityCombo.currentIndex)
}

// Progress bar (only while busy)
ProgressBar {
id: mgProgress
onClicked: {
var steps = [{ key: "prep", label: "Prepare models" }]
if (mgRemoveBg.checked)
steps.push({ key: "background", label: "Remove background" })
steps.push({ key: "encode", label: "Encode image" })
steps.push({ key: "decode", label: "Reconstruct 3D" })
if (mgRefine.checked)
steps.push({ key: "refine", label: "Refine surface" })
if (mgBake.checked)
steps.push({ key: "bake", label: "Bake texture" })
else
steps.push({ key: "color", label: "Vertex colors" })
if (mgUpscale.checked && mgBake.checked)
steps.push({ key: "upscale", label: "Upscale texture 2×" })
steps.push({ key: "build",
label: (mgPbr.checked && mgBake.checked)
? "Build mesh + PBR maps" : "Build mesh" })
mgRoot.mgSteps = steps
mgRoot.mgActiveIdx = 0
mgRoot.mgActiveProgress = -1

MeshGenController.generateSelected(
mgResCombo.resValue, mgRemoveBg.checked, mgQualityCombo.currentIndex,
{
"smooth": mgSmooth.checked,
"refine": mgRefine.checked,
"bake_texture": mgBake.checked,
"generate_pbr": mgPbr.checked,
"upscale_texture": mgUpscale.checked
})
}
Comment on lines +1800 to +1830

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

generate_pbr/upscale_texture aren't gated by bake_texture in the outgoing request, unlike the step-list label right above.

Line 1815 correctly computes the "build" step label as (mgPbr.checked && mgBake.checked), acknowledging PBR/upscale depend on baking. But mgPbr/mgUpscale stay checked (just visually disabled) if the user unchecks "Bake diffuse texture" after having checked them — and the payload below sends their raw checked values regardless of mgBake.checked, so generate_pbr/upscale_texture could be sent true while bake_texture is false.

♻️ Proposed fix
                     MeshGenController.generateSelected(
                         mgResCombo.resValue, mgRemoveBg.checked, mgQualityCombo.currentIndex,
                         {
                             "smooth": mgSmooth.checked,
                             "refine": mgRefine.checked,
                             "bake_texture": mgBake.checked,
-                            "generate_pbr": mgPbr.checked,
-                            "upscale_texture": mgUpscale.checked
+                            "generate_pbr": mgPbr.checked && mgBake.checked,
+                            "upscale_texture": mgUpscale.checked && mgBake.checked
                         })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onClicked: {
var steps = [{ key: "prep", label: "Prepare models" }]
if (mgRemoveBg.checked)
steps.push({ key: "background", label: "Remove background" })
steps.push({ key: "encode", label: "Encode image" })
steps.push({ key: "decode", label: "Reconstruct 3D" })
if (mgRefine.checked)
steps.push({ key: "refine", label: "Refine surface" })
if (mgBake.checked)
steps.push({ key: "bake", label: "Bake texture" })
else
steps.push({ key: "color", label: "Vertex colors" })
if (mgUpscale.checked && mgBake.checked)
steps.push({ key: "upscale", label: "Upscale texture 2×" })
steps.push({ key: "build",
label: (mgPbr.checked && mgBake.checked)
? "Build mesh + PBR maps" : "Build mesh" })
mgRoot.mgSteps = steps
mgRoot.mgActiveIdx = 0
mgRoot.mgActiveProgress = -1
MeshGenController.generateSelected(
mgResCombo.resValue, mgRemoveBg.checked, mgQualityCombo.currentIndex,
{
"smooth": mgSmooth.checked,
"refine": mgRefine.checked,
"bake_texture": mgBake.checked,
"generate_pbr": mgPbr.checked,
"upscale_texture": mgUpscale.checked
})
}
MeshGenController.generateSelected(
mgResCombo.resValue, mgRemoveBg.checked, mgQualityCombo.currentIndex,
{
"smooth": mgSmooth.checked,
"refine": mgRefine.checked,
"bake_texture": mgBake.checked,
"generate_pbr": mgPbr.checked && mgBake.checked,
"upscale_texture": mgUpscale.checked && mgBake.checked
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qml/PropertiesPanel.qml` around lines 1800 - 1830, The outgoing request from
PropertiesPanel.qml is sending generate_pbr and upscale_texture directly from
mgPbr.checked and mgUpscale.checked even when mgBake.checked is false, which can
conflict with the build-step logic. Update the payload passed to
MeshGenController.generateSelected so these flags are only true when
bake_texture is enabled, mirroring the existing step label gating around
mgBake.checked. Use the existing mgPbr, mgUpscale, and mgBake checks in the
onClicked handler to keep the request consistent with the UI state.

}

// Per-step progress list (only while busy): every selected stage
// gets its own row — ✓ when done, a live bar while active, dimmed
// while pending — so it's always clear WHICH phase is running.
Column {
width: parent.width - 16
visible: MeshGenController.busy
from: 0; to: 1
indeterminate: value <= 0
value: 0
spacing: 3
visible: MeshGenController.busy && mgRoot.mgSteps.length > 0

Repeater {
model: mgRoot.mgSteps

delegate: Item {
required property var modelData
required property int index
readonly property bool stepDone: index < mgRoot.mgActiveIdx
readonly property bool stepActive: index === mgRoot.mgActiveIdx
width: parent ? parent.width : 200
height: 18

Text {
id: stepLabel
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: (stepDone ? "✓ " : "• ") + modelData.label
color: PropertiesPanelController.textColor
opacity: stepDone ? 0.9 : (stepActive ? 1.0 : 0.4)
font.pixelSize: 10
font.bold: stepActive
}

// Mini per-step bar: full when done, live fraction while
// active (pulsing when the stage can't report a total).
Rectangle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 70
height: 5
radius: 2
color: PropertiesPanelController.inputColor
border.color: PropertiesPanelController.borderColor
border.width: 1
Rectangle {
id: stepFill
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
radius: 2
color: PropertiesPanelController.highlightColor
width: stepDone ? parent.width
: stepActive
? (mgRoot.mgActiveProgress >= 0
? Math.max(3, parent.width * mgRoot.mgActiveProgress)
: parent.width)
: 0
opacity: stepActive && mgRoot.mgActiveProgress < 0 ? 0.35 : 1.0
SequentialAnimation on opacity {
running: stepActive && mgRoot.mgActiveProgress < 0
loops: Animation.Infinite
NumberAnimation { from: 0.2; to: 0.6; duration: 600 }
NumberAnimation { from: 0.6; to: 0.2; duration: 600 }
}
}
}
}
}
}

Text {
Expand All @@ -1785,14 +1917,23 @@ Rectangle {
Connections {
target: MeshGenController
function onProgress(stage, done, total) {
if (total > 0 && done >= 0) {
mgProgress.indeterminate = (stage === "prep" || stage === "background")
mgProgress.value = total > 0 ? (done / total) : 0
// Advance the step list. Stages not in the list (e.g. the
// vertex-colour fallback after a failed bake) are ignored.
var idx = -1
for (var i = 0; i < mgRoot.mgSteps.length; i++)
if (mgRoot.mgSteps[i].key === stage) { idx = i; break }
if (idx < 0)
return
if (idx > mgRoot.mgActiveIdx) {
mgRoot.mgActiveIdx = idx
mgRoot.mgActiveProgress = -1
}
if (idx === mgRoot.mgActiveIdx)
mgRoot.mgActiveProgress = total > 0 ? done / total : -1
}
Comment on lines 1919 to 1933

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

onProgress doesn't honor the "total<=0 = no bar update" contract, risking flicker on the Bake stage.

MeshGenPredictor::ProgressFn documents total <= 0 as a pure cancellation check that should not affect the bar. Here, any call for the active stage unconditionally sets mgActiveProgress = total > 0 ? done / total : -1. For Stage::Bake specifically, the C++ side feeds two producers into this same stage: the baker's own accurate texel progress (bakeOpts.progress) and the color-sampler's cancel-only pings (sampleBuffer(..., report=false), always (-1,-1)). Every cancel-only ping will reset a real, in-progress fraction back to indeterminate, causing the bake progress bar to flicker between a real percentage and the pulsing/indeterminate state.

🐛 Proposed fix
                     if (idx > mgRoot.mgActiveIdx) {
                         mgRoot.mgActiveIdx = idx
                         mgRoot.mgActiveProgress = -1
                     }
-                    if (idx === mgRoot.mgActiveIdx)
-                        mgRoot.mgActiveProgress = total > 0 ? done / total : -1
+                    // total <= 0 is a pure cancellation-check ping (see
+                    // MeshGenPredictor::ProgressFn) — don't clobber a real
+                    // in-progress fraction with indeterminate on those.
+                    if (idx === mgRoot.mgActiveIdx && total > 0)
+                        mgRoot.mgActiveProgress = done / total
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function onProgress(stage, done, total) {
if (total > 0 && done >= 0) {
mgProgress.indeterminate = (stage === "prep" || stage === "background")
mgProgress.value = total > 0 ? (done / total) : 0
// Advance the step list. Stages not in the list (e.g. the
// vertex-colour fallback after a failed bake) are ignored.
var idx = -1
for (var i = 0; i < mgRoot.mgSteps.length; i++)
if (mgRoot.mgSteps[i].key === stage) { idx = i; break }
if (idx < 0)
return
if (idx > mgRoot.mgActiveIdx) {
mgRoot.mgActiveIdx = idx
mgRoot.mgActiveProgress = -1
}
if (idx === mgRoot.mgActiveIdx)
mgRoot.mgActiveProgress = total > 0 ? done / total : -1
}
function onProgress(stage, done, total) {
// Advance the step list. Stages not in the list (e.g. the
// vertex-colour fallback after a failed bake) are ignored.
var idx = -1
for (var i = 0; i < mgRoot.mgSteps.length; i++)
if (mgRoot.mgSteps[i].key === stage) { idx = i; break }
if (idx < 0)
return
if (idx > mgRoot.mgActiveIdx) {
mgRoot.mgActiveIdx = idx
mgRoot.mgActiveProgress = -1
}
// total <= 0 is a pure cancellation-check ping (see
// MeshGenPredictor::ProgressFn) — don't clobber a real
// in-progress fraction with indeterminate on those.
if (idx === mgRoot.mgActiveIdx && total > 0)
mgRoot.mgActiveProgress = done / total
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qml/PropertiesPanel.qml` around lines 1919 - 1933, The onProgress handler in
PropertiesPanel.qml is resetting mgActiveProgress to indeterminate whenever
total <= 0, which breaks the “cancel-check only” contract and can cause
Bake-stage flicker. Update onProgress so it only assigns mgActiveProgress when
total > 0, and ignore total <= 0 calls entirely for the active stage; keep the
existing stage lookup and mgActiveIdx transition logic in sync with this
behavior.

function onStatusMessage(msg) { mgStatus.text = msg }
function onCompleted(result) {
mgProgress.value = 1
mgRoot.mgActiveIdx = mgRoot.mgSteps.length // all ✓
mgStatus.text = "Done: " + result.vertexCount + " verts, "
+ result.triangleCount + " tris"
}
Expand Down
Loading
Loading