Skip to main content

quantum_sdk/
mesh.rs

1//! 3D model pipeline via Meshy: generate → remesh → retexture → rig → animate.
2//!
3//! All operations run through the async job system. Each method submits a job
4//! and polls until completion. Use the typed request structs or call
5//! [`Client::create_job`] directly with the appropriate `job_type`.
6
7use serde::{Deserialize, Serialize};
8
9use crate::client::Client;
10use crate::error::Result;
11use crate::jobs::{JobCreateRequest, JobStatusResponse};
12
13/// Request for a 3D remesh operation.
14///
15/// Submit via `client.remesh()` or via `client.create_job()` with
16/// `job_type: "3d/remesh"`.
17#[derive(Debug, Clone, Serialize, Default)]
18pub struct RemeshRequest {
19    /// ID of a completed 3D generation task (from Meshy).
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub input_task_id: Option<String>,
22
23    /// Direct URL to a 3D model file (alternative to input_task_id).
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub model_url: Option<String>,
26
27    /// Output formats: "glb", "fbx", "obj", "usdz", "stl", "blend".
28    /// Default: ["glb", "stl"].
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub target_formats: Option<Vec<String>>,
31
32    /// Mesh topology: "quad" or "triangle".
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub topology: Option<String>,
35
36    /// Target polygon count (100–300,000). Default: 30000.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub target_polycount: Option<i32>,
39
40    /// Resize height in meters (0 = no resize).
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub resize_height: Option<f64>,
43
44    /// Origin placement: "bottom", "center", or "" (no change).
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub origin_at: Option<String>,
47
48    /// If true, skip remeshing and only convert formats.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub convert_format_only: Option<bool>,
51}
52
53/// URLs for each exported format in a remesh result.
54#[derive(Debug, Clone, Deserialize, Default)]
55pub struct ModelUrls {
56    #[serde(default)]
57    pub glb: String,
58    #[serde(default)]
59    pub fbx: String,
60    #[serde(default)]
61    pub obj: String,
62    #[serde(default)]
63    pub usdz: String,
64    #[serde(default)]
65    pub stl: String,
66    #[serde(default)]
67    pub blend: String,
68}
69
70/// Request for AI retexturing of an existing 3D model.
71#[derive(Debug, Clone, Serialize, Default)]
72pub struct RetextureRequest {
73    /// ID of a completed 3D task to retexture.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub input_task_id: Option<String>,
76
77    /// Direct URL to a 3D model file.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub model_url: Option<String>,
80
81    /// Text prompt describing the desired texture.
82    pub prompt: String,
83
84    /// Enable PBR texture maps (metallic, roughness, normal).
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub enable_pbr: Option<bool>,
87
88    /// Meshy AI model to use (default: "meshy-6").
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub ai_model: Option<String>,
91}
92
93/// Request for auto-rigging a humanoid 3D model.
94#[derive(Debug, Clone, Serialize, Default)]
95pub struct RigRequest {
96    /// ID of a completed 3D task.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub input_task_id: Option<String>,
99
100    /// Direct URL to a 3D model file.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub model_url: Option<String>,
103
104    /// Height of the character in meters (for skeleton scaling).
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub height_meters: Option<f64>,
107}
108
109/// Request for applying an animation to a rigged character.
110#[derive(Debug, Clone, Serialize, Default)]
111pub struct AnimateRequest {
112    /// ID of a completed rigging task.
113    pub rig_task_id: String,
114
115    /// Animation action ID from Meshy's animation library.
116    pub action_id: i32,
117
118    /// Optional post-processing (e.g. FPS conversion, format conversion).
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub post_process: Option<AnimationPostProcess>,
121}
122
123/// Post-processing options for animation export.
124#[derive(Debug, Clone, Serialize, Default)]
125pub struct AnimationPostProcess {
126    /// Operation: "change_fps", "fbx2usdz", "extract_armature".
127    pub operation_type: String,
128    /// Target FPS (for "change_fps"): 24, 25, 30, 60.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub fps: Option<i32>,
131}
132
133/// Backwards-compatible alias for [`AnimationPostProcess`].
134pub type PostProcess = AnimationPostProcess;
135
136/// Request for 3D model generation (alias for [`crate::image::ImageRequest`]
137/// which includes Meshy 3D fields like topology, target_polycount, etc.).
138pub type Generate3DRequest = crate::image::ImageRequest;
139
140/// URLs for basic pre-built animations from a rigging result.
141#[derive(Debug, Clone, Deserialize, Default)]
142pub struct BasicAnimations {
143    /// Walking animation in GLB format.
144    #[serde(default)]
145    pub walking_glb: String,
146
147    /// Walking animation in FBX format.
148    #[serde(default)]
149    pub walking_fbx: String,
150
151    /// Running animation in GLB format.
152    #[serde(default)]
153    pub running_glb: String,
154
155    /// Running animation in FBX format.
156    #[serde(default)]
157    pub running_fbx: String,
158
159    /// Idle animation in GLB format.
160    #[serde(default)]
161    pub idle_glb: String,
162
163    /// Idle animation in FBX format.
164    #[serde(default)]
165    pub idle_fbx: String,
166}
167
168// ── Convenience methods ──
169
170impl Client {
171    /// Submit a 3D remesh job and poll until completion.
172    ///
173    /// Returns the job result containing `model_urls` with download links
174    /// for each requested format (including STL for 3D printing).
175    pub async fn remesh(&self, req: &RemeshRequest) -> Result<JobStatusResponse> {
176        self.submit_and_poll("3d/remesh", req).await
177    }
178
179    /// Submit a retexture job — apply new AI-generated textures to a 3D model.
180    ///
181    /// Returns the job result containing `model_urls` with the retextured model.
182    pub async fn retexture(&self, req: &RetextureRequest) -> Result<JobStatusResponse> {
183        self.submit_and_poll("3d/retexture", req).await
184    }
185
186    /// Submit a rigging job — add a humanoid skeleton to a 3D model.
187    ///
188    /// Returns the job result containing rigged FBX/GLB URLs and basic animations.
189    pub async fn rig(&self, req: &RigRequest) -> Result<JobStatusResponse> {
190        self.submit_and_poll("3d/rig", req).await
191    }
192
193    /// Submit an animation job — apply a motion to a rigged character.
194    ///
195    /// Returns the job result containing animated FBX/GLB URLs.
196    pub async fn animate(&self, req: &AnimateRequest) -> Result<JobStatusResponse> {
197        self.submit_and_poll("3d/animate", req).await
198    }
199
200    /// Internal: submit a job and poll until completion (shared by all 3D ops).
201    async fn submit_and_poll(
202        &self,
203        job_type: &str,
204        params: &impl serde::Serialize,
205    ) -> Result<JobStatusResponse> {
206        let params = serde_json::to_value(params)?;
207
208        let create_resp = self
209            .create_job(&JobCreateRequest {
210                job_type: job_type.into(),
211                params,
212            })
213            .await?;
214
215        self.poll_job(
216            &create_resp.job_id,
217            std::time::Duration::from_secs(5),
218            120, // 10 minutes max
219        )
220        .await
221    }
222}