新增图集连通域与可配置网格切分
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 5m32s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 5m28s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 5m40s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 5m54s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m54s
Project CI / AI game creator shell Rust crates (push) Successful in 2m9s
Project CI / Native shell tests (push) Failing after 2m32s
Project CI / AI game creator shell web tests (push) Failing after 1m17s
Project CI / Repository checks (push) Successful in 3m22s
Project CI / Frontend tests (push) Successful in 4m32s
Project CI / Backend tests (push) Successful in 7m1s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 5m32s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 5m28s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 5m40s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 5m54s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m54s
Project CI / AI game creator shell Rust crates (push) Successful in 2m9s
Project CI / Native shell tests (push) Failing after 2m32s
Project CI / AI game creator shell web tests (push) Failing after 1m17s
Project CI / Repository checks (push) Successful in 3m22s
Project CI / Frontend tests (push) Successful in 4m32s
Project CI / Backend tests (push) Successful in 7m1s
增加 connected-components 与 grid 切分模式 支持 gridX/gridY 并同步 API、MCP、Skill、AGC 客户端 移除固定 2x2 图集切分契约与文档
This commit is contained in:
@@ -32,6 +32,7 @@ Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses on
|
||||
- Use stable references such as `objectKey`, project resource ID, or asset ID where each operation permits them. Image edit/redraw is stricter: `sourceReferenceId` accepts only a registered project resource ID or asset ID; upload confirmation alone is not enough. Use `/assets/read-url` only for temporary preview/download access.
|
||||
- Preserve both warning channels after completion. A general `warning` can coexist with `sliceWarning`; do not discard either.
|
||||
- Do not invent missing derivatives. A source-preserved warning means the main source remains usable but requested post-processing failed. A slice warning means the complete transparent sheet is usable but individual slices are absent.
|
||||
- Icon spritesheet generation accepts `sliceMode="connected-components"` (default alpha-connectivity detection) or `sliceMode="grid"`. Grid mode requires `gridX` and `gridY` (1-32); use `sliceCount` only to constrain connected-component output.
|
||||
- For successful `style="pixelArt"`, treat completed-result and nested resource/asset dimensions as the final logical-grid PNG dimensions. They may differ from `size`, `imageSize`, the provider image, and `canvasCompletion.placeholder`; do not rescale or reject the artifact to match those inputs.
|
||||
- Keep generated artifacts in the canvas and asset library together. Character animation accepts `assetFolderId` and `assetLabel`; its completed result directly returns the final `assetKind="character-animation"` resource and asset with formal sequence fields. Do not create a duplicate first-frame record.
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
|
||||
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
|
||||
| Background removal | `/api/external/v1/editor/images/background-removals` | `sourceImageSrc` | `projectId`, `sourceResourceId`, `targetLayerId`, static-image `assetKind`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceLayout`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceMode`, `gridX`, `gridY`, `sliceCount`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
|
||||
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
@@ -94,7 +94,7 @@ For image edit/redraw, confirming an upload is not sufficient: create a project
|
||||
|
||||
The icon-spritesheet primary `referenceId` is intentionally stricter than ordinary image references: it accepts only a current-owner project resource ID or asset ID whose authoritative `assetKind` is `icon-spec`. It does not accept an `objectKey`, URL, Data URL, or Blob URL.
|
||||
|
||||
`sliceLayout: "grid-2x2"` is an opt-in contract for four fixed game-runtime assets. The provider prompt and server persistence both preserve the ordered slots left-top, right-top, left-bottom, right-bottom. Omit it to retain the default connected-component slicing behaviour for ordinary free-form icon sheets.
|
||||
`sliceMode` controls atlas splitting. Use `"connected-components"` (default) to detect independent opaque regions by alpha connectivity, or `"grid"` with positive `gridX` and `gridY` values (maximum 32 each). `sliceCount` optionally constrains the connected-component result.
|
||||
|
||||
## Common Values
|
||||
|
||||
|
||||
@@ -79,9 +79,9 @@ Keep the existing autonomous-build task graph. Do not add a parallel task system
|
||||
|
||||
1. `art-director` generates `assets/art-spec.png` with image generation, `kind: "spec"`, then registers it as `assetKind: "icon-spec"`. This image is the authoritative visual spec; `generationInputs.artSpec` is supporting structured context.
|
||||
2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`.
|
||||
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`. For the four-category game contract it must also send `sliceLayout: "grid-2x2"`; this is an explicit fixed-slot contract, not a client-side guessed crop.
|
||||
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`. For a fixed four-category game contract it may send `sliceMode: "grid"`; for free-form assets use `sliceMode: "connected-components"` (the default).
|
||||
|
||||
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require response `sliceLayout: "grid-2x2"` and exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
|
||||
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. When using the fixed four-category contract, require response `sliceMode: "grid"` and exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
|
||||
|
||||
Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG.
|
||||
|
||||
|
||||
@@ -881,12 +881,14 @@ def _self_test() -> None:
|
||||
["蛇头向上", "蛇身直线", "转角", "尾部", "四类食物"],
|
||||
canvasSession=session,
|
||||
assetLabel="贪吃蛇透明图集",
|
||||
sliceMode="connected-components",
|
||||
referenceId="must-not-override-explicit-reference",
|
||||
iconDescriptions=["不得覆盖显式图标描述"],
|
||||
)
|
||||
assert calls[0]["path"] == "/api/external/v1/editor/icon-spritesheets/generations"
|
||||
assert calls[0]["body"]["referenceId"] == "editor-resource-spec"
|
||||
assert calls[0]["body"]["screenColor"] == "auto"
|
||||
assert calls[0]["body"]["sliceMode"] == "connected-components"
|
||||
assert calls[0]["body"]["iconDescriptions"][0] == "蛇头向上"
|
||||
assert calls[1]["path"] == "/api/external/v1/generations/task-operation-demo"
|
||||
print("self-test ok")
|
||||
|
||||
@@ -19,6 +19,12 @@ image, UI design image, or publication material; use `agc_edit_image` for an
|
||||
edit of an existing registered image; use `taonier_prepare_game_art` only for
|
||||
the complete game-art package and its canonical slices.
|
||||
|
||||
When `agc_generate_image` is used with `kind="art-spritesheet"`, pass
|
||||
`sliceMode="connected-components"` (the default alpha-connectivity splitter)
|
||||
or `sliceMode="grid"` with `gridX` and `gridY` (1-32 each). The selected mode is carried
|
||||
through the client request and returned result; do not infer it from the number
|
||||
of slices.
|
||||
|
||||
## Authorization boundary
|
||||
|
||||
`agc_tools` is an AGC client-owned bridge to the AGC backend. In the normal client build it uses the current client login session and account routes; the user and model never need to provide, configure, paste, create, or rotate an API Key, Token, Cookie, URL, or `.env` value. If the tool returns `401` or `403`, report only that the AGC client login or permission state is unavailable, stop the operation, and do not ask the user for credentials or expose an internal URL.
|
||||
|
||||
+1
@@ -15,6 +15,7 @@
|
||||
- On timeout or uncertain delivery, reuse the recorded operation; never create a replacement request.
|
||||
- `postprocess-failed-source-preserved` means the complete provider source remains usable, but the requested transparent derivative is absent.
|
||||
- `sliceWarning` means the complete transparent sheet remains usable, but individual slices are absent.
|
||||
- For direct `agc_generate_image` spritesheet requests, `sliceMode="connected-components"` selects alpha-connectivity detection and `sliceMode="grid"` uses the caller-provided `gridX` and `gridY` (1-32 each). The client preserves the selected mode and grid dimensions in the request identity and result metadata.
|
||||
- General and slice warnings can coexist. The tool returns them separately through `warnings` and `sliceWarnings`; callers must preserve every entry and must not downgrade a slice warning into a successful independent-asset claim.
|
||||
- `assetPaths` contains the complete package paths. `slicePaths` contains only slices that the client downloaded, validated, and registered with their platform source identities.
|
||||
- `resources` contains only safe registered identity fields: local asset/path/kind/media type, Canvas project/resource/asset/task IDs, and reference resource IDs. It never exposes prompts, models, provider routes, absolute paths, URLs, tokens, cookies, or API keys.
|
||||
|
||||
@@ -549,6 +549,7 @@ fn extract_mcp_arguments(root: &Path, tool: &str, arguments: &Value) -> Value {
|
||||
}
|
||||
"agc_generate_image" => {
|
||||
copy_string(object, "kind", &mut out);
|
||||
copy_string(object, "sliceMode", &mut out);
|
||||
copy_string(object, "aspectRatio", &mut out);
|
||||
copy_string(object, "imageSize", &mut out);
|
||||
copy_string(object, "assetName", &mut out);
|
||||
|
||||
@@ -2783,6 +2783,9 @@ async fn generate_direct_taonier_art_asset_at(
|
||||
asset_label: asset_label.to_string(),
|
||||
replace_existing: root.join(output_path).is_file(),
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
};
|
||||
let runtime_context =
|
||||
direct_taonier_art_generation_runtime_context(root, output_path, asset_kind)?;
|
||||
|
||||
@@ -2104,6 +2104,9 @@ async fn bridge_generate_image(state: &DirectToolBridgeState, arguments: &Value)
|
||||
"imageSize",
|
||||
"assetName",
|
||||
"outputPath",
|
||||
"sliceMode",
|
||||
"gridX",
|
||||
"gridY",
|
||||
],
|
||||
)?;
|
||||
enforce_project_permission_policy(&state.root, "canvas.asset_generate")?;
|
||||
@@ -2145,6 +2148,44 @@ async fn bridge_generate_image(state: &DirectToolBridgeState, arguments: &Value)
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| "AI 生成图片".to_string());
|
||||
let output_path = bridge_optional_bounded_string(arguments, "outputPath", 512)?;
|
||||
let slice_mode = arguments
|
||||
.get("sliceMode")
|
||||
.map(|_| bridge_bounded_string(arguments, "sliceMode", 32))
|
||||
.transpose()?;
|
||||
if slice_mode
|
||||
.as_deref()
|
||||
.is_some_and(|mode| !matches!(mode, "connected-components" | "grid"))
|
||||
{
|
||||
return Err("工具参数 sliceMode 只允许 connected-components 或 grid".to_string());
|
||||
}
|
||||
let grid_x = arguments
|
||||
.get("gridX")
|
||||
.map(|_| {
|
||||
arguments
|
||||
.get("gridX")
|
||||
.and_then(Value::as_u64)
|
||||
.map(|value| value as u32)
|
||||
.ok_or_else(|| "工具参数 gridX 必须是整数".to_string())
|
||||
})
|
||||
.transpose()?;
|
||||
let grid_y = arguments
|
||||
.get("gridY")
|
||||
.map(|_| {
|
||||
arguments
|
||||
.get("gridY")
|
||||
.and_then(Value::as_u64)
|
||||
.map(|value| value as u32)
|
||||
.ok_or_else(|| "工具参数 gridY 必须是整数".to_string())
|
||||
})
|
||||
.transpose()?;
|
||||
if slice_mode.as_deref() == Some("grid") && (grid_x.is_none() || grid_y.is_none()) {
|
||||
return Err("grid 模式必须同时提供 gridX 与 gridY".to_string());
|
||||
}
|
||||
if grid_x.is_some_and(|value| !(1..=32).contains(&value))
|
||||
|| grid_y.is_some_and(|value| !(1..=32).contains(&value))
|
||||
{
|
||||
return Err("工具参数 gridX/gridY 必须在 1 到 32 之间".to_string());
|
||||
}
|
||||
let options = PlatformArtAssetGenerationOptions {
|
||||
output_path,
|
||||
aspect_ratio,
|
||||
@@ -2153,6 +2194,9 @@ async fn bridge_generate_image(state: &DirectToolBridgeState, arguments: &Value)
|
||||
asset_label: asset_name.clone(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
slice_mode,
|
||||
grid_x,
|
||||
grid_y,
|
||||
};
|
||||
let _generation_guard = state.image_generation_gate.lock().await;
|
||||
let generated = with_direct_editor_api_credentials(
|
||||
|
||||
@@ -244,6 +244,24 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
|
||||
"type": "string",
|
||||
"maxLength": 512,
|
||||
"description": "可选项目相对输出路径,必须位于 assets/ 且不能覆盖已有文件"
|
||||
},
|
||||
"sliceMode": {
|
||||
"type": "string",
|
||||
"enum": ["connected-components", "grid"],
|
||||
"default": "connected-components",
|
||||
"description": "仅 kind=art-spritesheet 生效:connected-components 按透明像素连通域切分,grid 按 gridX×gridY 网格切分"
|
||||
},
|
||||
"gridX": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 32,
|
||||
"description": "grid 模式横向网格数量"
|
||||
},
|
||||
"gridY": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 32,
|
||||
"description": "grid 模式纵向网格数量"
|
||||
}
|
||||
},
|
||||
"required": ["prompt"],
|
||||
@@ -1014,6 +1032,9 @@ async fn call_agc_generate_image(arguments: &Value) -> Value {
|
||||
"imageSize",
|
||||
"assetName",
|
||||
"outputPath",
|
||||
"sliceMode",
|
||||
"gridX",
|
||||
"gridY",
|
||||
],
|
||||
) {
|
||||
return mcp_tool_result(error, Vec::new(), true);
|
||||
@@ -1041,6 +1062,7 @@ async fn call_agc_generate_image(arguments: &Value) -> Value {
|
||||
("imageSize", 4),
|
||||
("assetName", DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS),
|
||||
("outputPath", 512),
|
||||
("sliceMode", 32),
|
||||
] {
|
||||
if arguments.get(field).is_some() {
|
||||
if let Err(error) = bounded_tool_string(arguments, field, max_chars) {
|
||||
@@ -2226,6 +2248,10 @@ mod tests {
|
||||
assert!(image_tool["description"]
|
||||
.as_str()
|
||||
.is_some_and(|description| description.contains("不是本工具的限制")));
|
||||
assert_eq!(
|
||||
image_tool["inputSchema"]["properties"]["sliceMode"]["enum"],
|
||||
json!(["connected-components", "grid"])
|
||||
);
|
||||
let edit_tool = specs["tools"]
|
||||
.as_array()
|
||||
.expect("tool array")
|
||||
|
||||
@@ -413,6 +413,9 @@ pub(crate) struct PlatformArtAssetGenerationOptions {
|
||||
pub(crate) asset_label: String,
|
||||
pub(crate) replace_existing: bool,
|
||||
pub(crate) slice_count: Option<usize>,
|
||||
pub(crate) slice_mode: Option<String>,
|
||||
pub(crate) grid_x: Option<u32>,
|
||||
pub(crate) grid_y: Option<u32>,
|
||||
}
|
||||
|
||||
impl Default for PlatformArtAssetGenerationOptions {
|
||||
@@ -425,6 +428,9 @@ impl Default for PlatformArtAssetGenerationOptions {
|
||||
asset_label: "AI 游戏首版美术素材".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1574,7 +1580,7 @@ pub(in crate::agent) struct PreparedPlatformArtAssetGeneration {
|
||||
warning: Option<String>,
|
||||
slice_warning: Option<String>,
|
||||
slices: Vec<PreparedPlatformArtAssetSlice>,
|
||||
spritesheet_slice_layout: Option<String>,
|
||||
spritesheet_slice_mode: Option<String>,
|
||||
generation_route: String,
|
||||
generation_kind: String,
|
||||
reference_resource_ids: Vec<String>,
|
||||
@@ -2213,11 +2219,8 @@ pub(crate) async fn generate_platform_art_asset_with_required_slices_at(
|
||||
/// 而任何输入不同(提示词、输出路径、比例、尺寸、类型、标签、严格切片)都是另一个
|
||||
/// 动作,必须各自独立成槽,才能在同一项目里同时在途。
|
||||
///
|
||||
/// **字段集合与取值方式必须与升级前逐字节一致**:升级前遗留账本里持久化的
|
||||
/// `actionFingerprint` 就是这个材料的历史哈希,改动材料会让旧账本无法按精确动作被
|
||||
/// 识别与迁移(见 `adopt_legacy_standalone_platform_art_generation_runtime_state_at`)。
|
||||
/// 已知边界:`slice_count` 不进身份(与升级前一致),仅切片数不同的两条图集请求仍落到
|
||||
/// 同一槽,第二条在账本请求正文校验处失败关闭,不会二次 POST。
|
||||
/// 升级前遗留账本仍由旧材料函数定位;新请求把显式切分模式纳入身份,避免同一图集
|
||||
/// 请求在网格与连通域之间误复用。`slice_count` 继续保持历史兼容语义,不进身份。
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StandalonePlatformArtGenerationFingerprintMaterial<'a> {
|
||||
@@ -2229,6 +2232,9 @@ struct StandalonePlatformArtGenerationFingerprintMaterial<'a> {
|
||||
asset_label: &'a str,
|
||||
replace_existing: bool,
|
||||
require_slices: bool,
|
||||
slice_mode: Option<&'a str>,
|
||||
grid_x: Option<u32>,
|
||||
grid_y: Option<u32>,
|
||||
}
|
||||
|
||||
/// 把输出路径收口成稳定的旧槽材料:空路径与未指定路径都落到 `(automatic-output)`,
|
||||
@@ -2265,6 +2271,9 @@ fn standalone_platform_art_generation_runtime_context(
|
||||
asset_label: &options.asset_label,
|
||||
replace_existing: options.replace_existing,
|
||||
require_slices,
|
||||
slice_mode: options.slice_mode.as_deref(),
|
||||
grid_x: options.grid_x,
|
||||
grid_y: options.grid_y,
|
||||
})
|
||||
.map_err(|error| format!("序列化 standalone 图片生成动作身份失败:{error}"))?;
|
||||
let action_fingerprint = format!("{:x}", Sha256::digest(&identity_bytes));
|
||||
@@ -2811,6 +2820,9 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
"referenceId": reference_id,
|
||||
"iconDescriptions": canonical_art_spritesheet_icon_descriptions(&generation_prompt),
|
||||
"sliceCount": options.slice_count,
|
||||
"sliceMode": options.slice_mode,
|
||||
"gridX": options.grid_x,
|
||||
"gridY": options.grid_y,
|
||||
"screenColor": "auto",
|
||||
"aspectRatio": options.aspect_ratio,
|
||||
"imageSize": options.image_size,
|
||||
@@ -3106,8 +3118,8 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
Vec::new()
|
||||
};
|
||||
let warning = platform_art_generation_warning(generated);
|
||||
let spritesheet_slice_layout = if is_canonical_art_spritesheet {
|
||||
json_string_field(generated, "sliceLayout")
|
||||
let spritesheet_slice_mode = if is_canonical_art_spritesheet {
|
||||
json_string_field(generated, "sliceMode")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -3168,7 +3180,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
warning,
|
||||
slice_warning,
|
||||
slices,
|
||||
spritesheet_slice_layout,
|
||||
spritesheet_slice_mode,
|
||||
generation_route,
|
||||
generation_kind,
|
||||
reference_resource_ids,
|
||||
@@ -6508,7 +6520,7 @@ fn validate_strict_platform_art_spritesheet_contract(
|
||||
task_id: Option<&str>,
|
||||
generation_route: &str,
|
||||
generation_kind: &str,
|
||||
spritesheet_slice_layout: Option<&str>,
|
||||
spritesheet_slice_mode: Option<&str>,
|
||||
reference_resource_ids: &[String],
|
||||
has_transparent_pixels: bool,
|
||||
has_visible_pixels: bool,
|
||||
@@ -6545,7 +6557,7 @@ fn validate_strict_platform_art_spritesheet_contract(
|
||||
{
|
||||
return Err("strict spritesheet 图集生成 route/kind 与严格图集合同不一致".to_string());
|
||||
}
|
||||
let _requested_slice_layout = spritesheet_slice_layout;
|
||||
let _requested_slice_mode = spritesheet_slice_mode;
|
||||
if reference_resource_ids.len() != 1
|
||||
|| reference_resource_ids[0].trim().is_empty()
|
||||
|| reference_resource_ids[0].trim() == resource_id
|
||||
@@ -7307,7 +7319,7 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
|
||||
warning,
|
||||
mut slice_warning,
|
||||
slices,
|
||||
spritesheet_slice_layout,
|
||||
spritesheet_slice_mode,
|
||||
generation_route,
|
||||
generation_kind,
|
||||
reference_resource_ids,
|
||||
@@ -7326,7 +7338,7 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
|
||||
task_id.as_deref(),
|
||||
&generation_route,
|
||||
&generation_kind,
|
||||
spritesheet_slice_layout.as_deref(),
|
||||
spritesheet_slice_mode.as_deref(),
|
||||
&reference_resource_ids,
|
||||
spritesheet_has_transparent_pixels,
|
||||
spritesheet_has_visible_pixels,
|
||||
@@ -7901,8 +7913,8 @@ mod canvas_generation_tests {
|
||||
let body = serde_json::json!({
|
||||
"error": {
|
||||
"code": "invalid-request",
|
||||
"field": "sliceLayout",
|
||||
"message": "只支持 grid-2x2;operationId=private-operation-id;api_key=private-key",
|
||||
"field": "sliceMode",
|
||||
"message": "只支持 grid;operationId=private-operation-id;api_key=private-key",
|
||||
},
|
||||
"details": {
|
||||
"path": "C:\\Users\\private\\secret.json",
|
||||
@@ -7911,8 +7923,8 @@ mod canvas_generation_tests {
|
||||
.to_string();
|
||||
let summary = summarize_external_http_error_body(&body).expect("summary");
|
||||
assert!(summary.contains("code=invalid-request"), "{summary}");
|
||||
assert!(summary.contains("field=sliceLayout"), "{summary}");
|
||||
assert!(summary.contains("只支持 grid-2x2"), "{summary}");
|
||||
assert!(summary.contains("field=sliceMode"), "{summary}");
|
||||
assert!(summary.contains("只支持 grid"), "{summary}");
|
||||
assert!(!summary.contains("private-operation-id"), "{summary}");
|
||||
assert!(!summary.contains("private-key"), "{summary}");
|
||||
assert!(!summary.contains("C:\\Users\\private"), "{summary}");
|
||||
@@ -8312,6 +8324,9 @@ mod canvas_generation_tests {
|
||||
asset_label: "手工背景".to_string(),
|
||||
replace_existing: true,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
};
|
||||
let ordinary =
|
||||
standalone_platform_art_generation_runtime_context("完整生成提示词", &options, false)
|
||||
@@ -9826,7 +9841,7 @@ mod canvas_generation_tests {
|
||||
Some("spritesheet-task"),
|
||||
"/api/external/v1/editor/icon-spritesheets/generations",
|
||||
"icon-spritesheet",
|
||||
Some("grid-2x2"),
|
||||
Some("grid"),
|
||||
&["art-spec-resource".to_string()],
|
||||
true,
|
||||
true,
|
||||
@@ -10325,6 +10340,9 @@ mod canvas_generation_tests {
|
||||
asset_label: "整包规范图".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
};
|
||||
let prompt = "生成同一套整包美术";
|
||||
let generation_prompt = build_platform_art_asset_prompt(prompt, &[], &options);
|
||||
@@ -11220,6 +11238,9 @@ mod canvas_generation_tests {
|
||||
asset_label: "整包背景图".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
};
|
||||
let prompt = "保持同一个生成提示词";
|
||||
let generation_prompt = build_platform_art_asset_prompt(prompt, &[], &options);
|
||||
@@ -11681,6 +11702,9 @@ mod canvas_generation_tests {
|
||||
asset_label: "游戏统一视觉规范图".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
};
|
||||
let prompt = "恢复已受理视觉规范图";
|
||||
let generation_prompt = build_platform_art_asset_prompt(prompt, &[], &options);
|
||||
@@ -12287,6 +12311,9 @@ mod canvas_generation_tests {
|
||||
asset_label: "游戏首版核心美术素材".to_string(),
|
||||
replace_existing: true,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12317,7 +12344,7 @@ mod canvas_generation_tests {
|
||||
warning: None,
|
||||
slice_warning: None,
|
||||
slices: Vec::new(),
|
||||
spritesheet_slice_layout: Some("grid-2x2".to_string()),
|
||||
spritesheet_slice_mode: Some("grid".to_string()),
|
||||
generation_route: "/api/external/v1/editor/icon-spritesheets/generations".to_string(),
|
||||
generation_kind: "icon-spritesheet".to_string(),
|
||||
reference_resource_ids: vec!["art-spec-resource".to_string()],
|
||||
@@ -12636,7 +12663,7 @@ mod canvas_generation_tests {
|
||||
warning: None,
|
||||
slice_warning: None,
|
||||
slices,
|
||||
spritesheet_slice_layout: Some("grid-2x2".to_string()),
|
||||
spritesheet_slice_mode: Some("grid".to_string()),
|
||||
generation_route: "/api/external/v1/editor/icon-spritesheets/generations".to_string(),
|
||||
generation_kind: "icon-spritesheet".to_string(),
|
||||
reference_resource_ids: vec!["art-spec-resource".to_string()],
|
||||
|
||||
@@ -555,6 +555,17 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
|
||||
.or_else(|| input.get("slice_count"))
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.map(|value| value as usize);
|
||||
let slice_mode = agent_runtime_tool_input_text(input, &["sliceMode", "slice_mode"]);
|
||||
let grid_x = input
|
||||
.get("gridX")
|
||||
.or_else(|| input.get("grid_x"))
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.map(|value| value as u32);
|
||||
let grid_y = input
|
||||
.get("gridY")
|
||||
.or_else(|| input.get("grid_y"))
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.map(|value| value as u32);
|
||||
let mut requested_options = PlatformArtAssetGenerationOptions {
|
||||
output_path: (!output_path.trim().is_empty()).then_some(output_path),
|
||||
aspect_ratio,
|
||||
@@ -563,6 +574,9 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
|
||||
asset_label,
|
||||
replace_existing,
|
||||
slice_count,
|
||||
slice_mode: (!slice_mode.trim().is_empty()).then_some(slice_mode.clone()),
|
||||
grid_x,
|
||||
grid_y,
|
||||
};
|
||||
if let Some(pending) = pending_action {
|
||||
match recover_persisted_visual_generation_options(
|
||||
@@ -609,6 +623,11 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
|
||||
},
|
||||
replace_existing,
|
||||
slice_count,
|
||||
slice_mode: requested_options
|
||||
.slice_mode
|
||||
.or_else(|| (!slice_mode.trim().is_empty()).then_some(slice_mode)),
|
||||
grid_x,
|
||||
grid_y,
|
||||
}
|
||||
};
|
||||
options.replace_existing = replace_existing;
|
||||
@@ -654,6 +673,28 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
if options
|
||||
.slice_mode
|
||||
.as_deref()
|
||||
.is_some_and(|slice_mode| !matches!(slice_mode, "connected-components" | "grid"))
|
||||
{
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "canvas.asset_generate".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: "图片生成 sliceMode 不受支持".to_string(),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
if options.slice_mode.as_deref() == Some("grid")
|
||||
&& (options.grid_x.is_none() || options.grid_y.is_none())
|
||||
{
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "canvas.asset_generate".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: "grid 模式必须同时提供 gridX 与 gridY".to_string(),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
if !agent_runtime_canvas_asset_kind_is_supported(&options.asset_kind) {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "canvas.asset_generate".to_string(),
|
||||
|
||||
@@ -4594,6 +4594,9 @@ pub(crate) fn prepare_local_project_asset_generation(
|
||||
.unwrap_or_else(|| LOCAL_PROJECT_ASSET_DEFAULT_ASSET_NAME.to_string()),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3192,7 +3192,9 @@ fn spawn_mock_external_canvas_api_server_with_capture_and_generation_gate(
|
||||
"spritesheetImageSrc": "/generated/canvas/spritesheet.png",
|
||||
"spritesheetWidth": 2,
|
||||
"spritesheetHeight": 1,
|
||||
"sliceLayout": "grid-2x2",
|
||||
"sliceMode": "grid",
|
||||
"gridX": 2,
|
||||
"gridY": 2,
|
||||
"iconImageSrcs": icon_image_srcs,
|
||||
"sliceWarning": null,
|
||||
"prompt": "原创游戏素材图集",
|
||||
|
||||
@@ -1076,6 +1076,9 @@ async fn canonical_art_spec_and_ui_requests_use_the_shared_reference_chain() {
|
||||
asset_label: "游戏横屏界面原型图".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@@ -5493,6 +5496,9 @@ fn ui_prototype_generation_uses_dedicated_prompt_and_art_spec() {
|
||||
asset_label: "游戏横屏界面原型图".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
};
|
||||
let prompt = build_platform_art_asset_prompt(
|
||||
"原创网格贪吃蛇:分数与状态 HUD、四类不同分值食物、开始、方向键/WASD、触控方向键、失败与重开",
|
||||
|
||||
@@ -3370,16 +3370,32 @@
|
||||
"maxLength": 200
|
||||
}
|
||||
},
|
||||
"sliceLayout": {
|
||||
"sliceMode": {
|
||||
"type": "string",
|
||||
"deprecated": true,
|
||||
"description": "历史兼容字段,新的调用请使用 sliceCount。"
|
||||
"enum": [
|
||||
"connected-components",
|
||||
"grid"
|
||||
],
|
||||
"default": "connected-components",
|
||||
"description": "图集切分模式。connected-components 按透明像素 alpha 连通域识别独立素材;grid 按用户提供的 gridX/gridY 划分网格槽。省略时使用 connected-components。"
|
||||
},
|
||||
"gridX": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 32,
|
||||
"description": "grid 模式的横向网格数量。"
|
||||
},
|
||||
"gridY": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 32,
|
||||
"description": "grid 模式的纵向网格数量。"
|
||||
},
|
||||
"sliceCount": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"description": "可选的目标切片数量;省略时按图像内容自动识别。"
|
||||
"description": "connected-components 模式下可选的目标切片数量;省略时按图像内容自动识别。grid 模式的切片数量由 gridX×gridY 决定。"
|
||||
},
|
||||
"screenColor": {
|
||||
"type": ["string", "null"],
|
||||
@@ -3603,15 +3619,28 @@
|
||||
},
|
||||
"iconImageSrcs": {
|
||||
"type": "array",
|
||||
"description": "识别图集中有效 alpha 连通域并持久化的独立素材,按视觉阅读顺序命名为“素材 N”;可通过 sliceCount 指定目标数量。",
|
||||
"description": "按 sliceMode 识别或裁切并持久化的独立素材,按视觉阅读顺序命名为“素材 N”;connected-components 模式可通过 sliceCount 指定目标数量。",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EditorIconSpritesheetIconResult"
|
||||
}
|
||||
},
|
||||
"sliceLayout": {
|
||||
"sliceMode": {
|
||||
"type": "string",
|
||||
"deprecated": true,
|
||||
"description": "历史兼容字段。"
|
||||
"enum": [
|
||||
"connected-components",
|
||||
"grid"
|
||||
],
|
||||
"description": "实际采用的图集切分模式。"
|
||||
},
|
||||
"gridX": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 32
|
||||
},
|
||||
"gridY": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 32
|
||||
},
|
||||
"sliceCount": {
|
||||
"type": "integer",
|
||||
@@ -3628,7 +3657,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "可信透明图集已成功持久化,但全连通域自动拆分未完成时返回;此时 iconImageSrcs 为空,调用方仍应使用整张图集。原始连通域、输出数量或 CPU 预算超限不会产生切片 PUT、资源或画布切片。透明处理、Alpha/尺寸恢复、provider 原图修复性回读或透明图完整解码失败时走 provider 原图 source-only,sliceWarning 为 null。"
|
||||
"description": "可信透明图集已成功持久化,但所选 sliceMode 的自动拆分未完成时返回;此时 iconImageSrcs 为空,调用方仍应使用整张图集。原始连通域、输出数量、网格裁切或 CPU 预算超限不会产生切片 PUT、资源或画布切片。透明处理、Alpha/尺寸恢复、provider 原图修复性回读或透明图完整解码失败时走 provider 原图 source-only,sliceWarning 为 null。"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
## 生成契约
|
||||
|
||||
- 前端提交到 `POST /api/editor/icon-spritesheets/generations`。
|
||||
- 图集拆分通过 `sliceMode` 显式选择:`connected-components` 按透明像素连通域切分(默认),`grid` 按用户提供的 `gridX × gridY` 网格切分。
|
||||
- 图标规范生成在 inline 模式下也必须先建立带稳定请求指纹的 generation operation,并由编辑器生成 durable billing 边界包住共享执行器;不得在 `operation=None` 时调用 provider 后再进入原子结果持久化。
|
||||
- 图标 spritesheet 的入队与实际执行路径都必须在引用解析、generation input 重建、定价和 provider / OSS 副作用之前预检 owner、项目和最终素材目录,并将返回的 canonical `projectId + assetFolderId` 回写到后续流程;请求省略目录时按实际写入的 owner 默认目录预检,worker 不得只信任入队时的旧校验结果。
|
||||
- queued 图标规范生成由共享原子结果持久化使用 worker caller 中的 lease 一并完成任务并清理 lease;共享执行器返回成功后 worker 只能返回 `Ok(())`,不得再次调用 job completion。
|
||||
|
||||
@@ -864,7 +864,9 @@ impl EditorAgentTool for GenerateIconSpritesheetTool {
|
||||
reference_image_srcs: Some(reference_image_srcs),
|
||||
icon_descriptions: args.icon_descriptions,
|
||||
slice_count: None,
|
||||
slice_layout: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
style: None,
|
||||
model: Some(args.model),
|
||||
screen_color: Some("auto".to_string()),
|
||||
|
||||
@@ -93,7 +93,8 @@ use crate::{
|
||||
},
|
||||
editor_project_icon::{
|
||||
EditorIconSpritesheetGenerationResponse, EditorIconSpritesheetIconResponse,
|
||||
PersistEditorSpritesheetSlicesInput, editor_icon_spritesheet_slice_warning_from_error,
|
||||
EditorIconSpritesheetSliceMode, PersistEditorSpritesheetSlicesInput,
|
||||
editor_icon_spritesheet_slice_warning_from_error,
|
||||
editor_icon_spritesheet_warning_after_persist_error,
|
||||
prepare_editor_spritesheet_slices_for_generation, slice_editor_icon_spritesheet_all,
|
||||
},
|
||||
@@ -1267,7 +1268,7 @@ fn compact_external_api_generation_result(result: Value) -> Value {
|
||||
| "spritesheetWidth"
|
||||
| "spritesheetHeight"
|
||||
| "iconImageSrcs"
|
||||
| "sliceLayout"
|
||||
| "sliceMode"
|
||||
| "frames"
|
||||
| "frameCount"
|
||||
| "frameWidth"
|
||||
@@ -8578,7 +8579,9 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner(
|
||||
spritesheet_width: source_width,
|
||||
spritesheet_height: source_height,
|
||||
icon_image_srcs: Vec::new(),
|
||||
slice_layout: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
slice_count: None,
|
||||
slice_warning: None,
|
||||
prompt,
|
||||
@@ -8645,7 +8648,9 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner(
|
||||
spritesheet_width: source_width,
|
||||
spritesheet_height: source_height,
|
||||
icon_image_srcs: Vec::new(),
|
||||
slice_layout: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
slice_count: None,
|
||||
slice_warning: None,
|
||||
prompt,
|
||||
@@ -8730,8 +8735,10 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner(
|
||||
let (mut icon_image_srcs, slice_items, slice_warning) = match slice_editor_icon_spritesheet_all(
|
||||
slice_source,
|
||||
request_context.external_call_deadline(),
|
||||
EditorIconSpritesheetSliceMode::ConnectedComponents,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -8893,7 +8900,9 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner(
|
||||
spritesheet_width,
|
||||
spritesheet_height,
|
||||
icon_image_srcs,
|
||||
slice_layout: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
slice_count: None,
|
||||
slice_warning,
|
||||
prompt,
|
||||
@@ -19063,10 +19072,17 @@ mod tests {
|
||||
.checked_sub(Duration::from_millis(1))
|
||||
.expect("expired deadline should be representable");
|
||||
|
||||
let error = slice_editor_icon_spritesheet_all(source, Some(expired), None, None)
|
||||
.await
|
||||
.err()
|
||||
.expect("expired CPU budget must fail before decoding");
|
||||
let error = slice_editor_icon_spritesheet_all(
|
||||
source,
|
||||
Some(expired),
|
||||
EditorIconSpritesheetSliceMode::ConnectedComponents,
|
||||
None,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.expect("expired CPU budget must fail before decoding");
|
||||
|
||||
assert_eq!(error.status_code(), StatusCode::GATEWAY_TIMEOUT);
|
||||
assert_eq!(
|
||||
@@ -19751,7 +19767,9 @@ mod tests {
|
||||
spritesheet_width: 512,
|
||||
spritesheet_height: 512,
|
||||
icon_image_srcs: Vec::new(),
|
||||
slice_layout: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
slice_count: None,
|
||||
slice_warning: Some(EditorIconSpritesheetSliceWarningResponse {
|
||||
code: EDITOR_ICON_SPRITESHEET_SLICE_WARNING_COMPONENTS,
|
||||
@@ -19984,7 +20002,6 @@ mod tests {
|
||||
fn atomic_job_result_keeps_only_the_target_consumer_contract() {
|
||||
let result = json!({
|
||||
"ok": true,
|
||||
"sliceLayout": "grid-2x2",
|
||||
"imageSrc": "/api/assets/object/generated.png",
|
||||
"objectKey": "generated/image.png",
|
||||
"width": 512,
|
||||
@@ -20069,7 +20086,6 @@ mod tests {
|
||||
);
|
||||
assert_eq!(external_payload["result"]["asset"]["assetId"], "asset-1");
|
||||
assert_eq!(external_payload["result"]["ok"], true);
|
||||
assert_eq!(external_payload["result"]["sliceLayout"], "grid-2x2");
|
||||
assert_eq!(external_payload["result"]["prompt"], "用户可见提示词");
|
||||
assert_eq!(
|
||||
external_payload["result"]["actualPrompt"],
|
||||
|
||||
@@ -14,7 +14,7 @@ use platform_image::{
|
||||
generated_asset_sheets::{
|
||||
GeneratedAssetSheetConnectedIcon, GeneratedAssetSheetConnectedIconPlan,
|
||||
GeneratedAssetSheetError, prepare_generated_icon_spritesheet_all_by_connected_components,
|
||||
prepare_generated_icon_spritesheet_grid_2x2,
|
||||
prepare_generated_icon_spritesheet_grid,
|
||||
},
|
||||
};
|
||||
use platform_llm::{EDITOR_AGENT_GPT5_MODEL, LlmMessage, LlmRunRequest};
|
||||
@@ -82,6 +82,7 @@ pub(crate) const EDITOR_ICON_SPRITESHEET_MEMORY_MAX_CONCURRENCY: usize = 2;
|
||||
pub(crate) const EDITOR_ICON_SPRITESHEET_UPLOAD_MAX_CONCURRENCY: usize = 2;
|
||||
pub(crate) const EDITOR_ICON_SPRITESHEET_MAX_TOTAL_CROP_PIXELS: u64 =
|
||||
EDITOR_ICON_SPRITESHEET_MAX_PIXELS * 4;
|
||||
const EDITOR_ICON_SPRITESHEET_MAX_GRID_AXIS: u32 = 32;
|
||||
pub(crate) const EDITOR_ICON_SPRITESHEET_UPLOAD_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
pub(crate) const EDITOR_ICON_SPRITESHEET_UPLOAD_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
pub(crate) const EDITOR_ICON_SPRITESHEET_MAX_PROCESSING_DURATION: Duration =
|
||||
@@ -254,8 +255,13 @@ pub(crate) struct EditorIconSpritesheetGenerationRequest {
|
||||
/// 用户要求的切片数量;未提供时按图像中的连通素材自动识别。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) slice_count: Option<usize>,
|
||||
/// 图集切分模式;省略时使用连通域切分。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) slice_layout: Option<EditorIconSpritesheetSliceLayout>,
|
||||
pub(crate) slice_mode: Option<EditorIconSpritesheetSliceMode>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) grid_x: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) grid_y: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) style: Option<String>,
|
||||
pub(crate) model: Option<String>,
|
||||
@@ -270,11 +276,50 @@ pub(crate) struct EditorIconSpritesheetGenerationRequest {
|
||||
pub(crate) canvas_completion: Option<EditorCanvasGenerationCompletionPayload>,
|
||||
}
|
||||
|
||||
/// Deprecated compatibility layout. New callers should use `sliceCount`。
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub(crate) enum EditorIconSpritesheetSliceLayout {
|
||||
#[serde(rename = "grid-2x2")]
|
||||
Grid2x2,
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub(crate) enum EditorIconSpritesheetSliceMode {
|
||||
ConnectedComponents,
|
||||
Grid,
|
||||
}
|
||||
|
||||
impl Default for EditorIconSpritesheetSliceMode {
|
||||
fn default() -> Self {
|
||||
Self::ConnectedComponents
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_editor_icon_spritesheet_slice_mode(
|
||||
slice_mode: Option<EditorIconSpritesheetSliceMode>,
|
||||
) -> EditorIconSpritesheetSliceMode {
|
||||
slice_mode.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn resolve_editor_icon_spritesheet_grid_dimensions(
|
||||
mode: EditorIconSpritesheetSliceMode,
|
||||
grid_x: Option<u32>,
|
||||
grid_y: Option<u32>,
|
||||
) -> Result<(u32, u32), AppError> {
|
||||
if mode == EditorIconSpritesheetSliceMode::ConnectedComponents {
|
||||
return Ok((0, 0));
|
||||
}
|
||||
let (Some(grid_x), Some(grid_y)) = (grid_x, grid_y) else {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"field": "gridX/gridY",
|
||||
"message": "grid 模式必须同时提供 gridX 与 gridY。",
|
||||
})),
|
||||
);
|
||||
};
|
||||
if !(1..=EDITOR_ICON_SPRITESHEET_MAX_GRID_AXIS).contains(&grid_x)
|
||||
|| !(1..=EDITOR_ICON_SPRITESHEET_MAX_GRID_AXIS).contains(&grid_y)
|
||||
{
|
||||
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"field": "gridX/gridY",
|
||||
"message": format!("gridX 与 gridY 必须在 1 到 {} 之间。", EDITOR_ICON_SPRITESHEET_MAX_GRID_AXIS),
|
||||
})));
|
||||
}
|
||||
Ok((grid_x, grid_y))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
@@ -313,7 +358,11 @@ pub(crate) struct EditorIconSpritesheetGenerationResponse {
|
||||
pub(crate) spritesheet_height: u32,
|
||||
pub(crate) icon_image_srcs: Vec<EditorIconSpritesheetIconResponse>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) slice_layout: Option<EditorIconSpritesheetSliceLayout>,
|
||||
pub(crate) slice_mode: Option<EditorIconSpritesheetSliceMode>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) grid_x: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) grid_y: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) slice_count: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -1579,6 +1628,12 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
|
||||
.or_else(|| payload.project_id.clone()),
|
||||
);
|
||||
let http_client = build_openai_image_http_client(&settings)?;
|
||||
let requested_slice_mode = resolve_editor_icon_spritesheet_slice_mode(payload.slice_mode);
|
||||
let (grid_x, grid_y) = resolve_editor_icon_spritesheet_grid_dimensions(
|
||||
requested_slice_mode,
|
||||
payload.grid_x,
|
||||
payload.grid_y,
|
||||
)?;
|
||||
// TODO(legacy-icon-spritesheet-billing-boundary): 该计费边界继承自 master 的历史实现;
|
||||
// Provider 成功后 operation 即提交,后续解码、OSS、资源与画布持久化失败时缺少可对账中间态。
|
||||
// 调整前需先定义 provider_succeeded/persistence_pending 等状态、稳定幂等键和补偿语义,
|
||||
@@ -1618,15 +1673,18 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
|
||||
)
|
||||
.await?;
|
||||
let screen_color = screen_background_decision.color;
|
||||
let prompt = match payload.slice_layout {
|
||||
Some(EditorIconSpritesheetSliceLayout::Grid2x2) => {
|
||||
crate::prompt::icon_spec::build_grid_2x2_spritesheet_prompt(
|
||||
let slice_mode = requested_slice_mode;
|
||||
let prompt = match slice_mode {
|
||||
EditorIconSpritesheetSliceMode::Grid => {
|
||||
crate::prompt::icon_spec::build_grid_spritesheet_prompt(
|
||||
&spritesheet_prompt,
|
||||
screen_color,
|
||||
icon_spec_genre,
|
||||
grid_x,
|
||||
grid_y,
|
||||
)
|
||||
}
|
||||
None => crate::prompt::icon_spec::build_spritesheet_prompt(
|
||||
EditorIconSpritesheetSliceMode::ConnectedComponents => crate::prompt::icon_spec::build_spritesheet_prompt(
|
||||
&spritesheet_prompt,
|
||||
screen_color,
|
||||
icon_spec_genre,
|
||||
@@ -1786,7 +1844,11 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
|
||||
spritesheet_width: source_width,
|
||||
spritesheet_height: source_height,
|
||||
icon_image_srcs: Vec::new(),
|
||||
slice_layout: payload.slice_layout,
|
||||
slice_mode: Some(requested_slice_mode),
|
||||
grid_x: (requested_slice_mode == EditorIconSpritesheetSliceMode::Grid)
|
||||
.then_some(grid_x),
|
||||
grid_y: (requested_slice_mode == EditorIconSpritesheetSliceMode::Grid)
|
||||
.then_some(grid_y),
|
||||
slice_count: Some(0),
|
||||
slice_warning: None,
|
||||
prompt,
|
||||
@@ -1868,7 +1930,11 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
|
||||
spritesheet_width: source_width,
|
||||
spritesheet_height: source_height,
|
||||
icon_image_srcs: Vec::new(),
|
||||
slice_layout: payload.slice_layout,
|
||||
slice_mode: Some(requested_slice_mode),
|
||||
grid_x: (requested_slice_mode == EditorIconSpritesheetSliceMode::Grid)
|
||||
.then_some(grid_x),
|
||||
grid_y: (requested_slice_mode == EditorIconSpritesheetSliceMode::Grid)
|
||||
.then_some(grid_y),
|
||||
slice_count: Some(0),
|
||||
slice_warning: None,
|
||||
prompt,
|
||||
@@ -1966,8 +2032,10 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
|
||||
let (mut icon_image_srcs, slice_items, slice_warning) = match slice_editor_icon_spritesheet_all(
|
||||
slice_source,
|
||||
request_context.external_call_deadline(),
|
||||
payload.slice_layout,
|
||||
requested_slice_mode,
|
||||
payload.slice_count,
|
||||
grid_x,
|
||||
grid_y,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -2059,7 +2127,12 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
|
||||
"spritesheetWidth": spritesheet_width,
|
||||
"spritesheetHeight": spritesheet_height,
|
||||
"iconImageSrcs": &icon_image_srcs,
|
||||
"sliceLayout": payload.slice_layout,
|
||||
|
||||
"sliceMode": requested_slice_mode,
|
||||
"gridX": (requested_slice_mode == EditorIconSpritesheetSliceMode::Grid)
|
||||
.then_some(grid_x),
|
||||
"gridY": (requested_slice_mode == EditorIconSpritesheetSliceMode::Grid)
|
||||
.then_some(grid_y),
|
||||
"sliceCount": payload.slice_count,
|
||||
"sliceWarning": &slice_warning,
|
||||
"warning": &generation_warning,
|
||||
@@ -2137,7 +2210,12 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
|
||||
spritesheet_width,
|
||||
spritesheet_height,
|
||||
icon_image_srcs,
|
||||
slice_layout: payload.slice_layout,
|
||||
|
||||
slice_mode: Some(requested_slice_mode),
|
||||
grid_x: (requested_slice_mode == EditorIconSpritesheetSliceMode::Grid)
|
||||
.then_some(grid_x),
|
||||
grid_y: (requested_slice_mode == EditorIconSpritesheetSliceMode::Grid)
|
||||
.then_some(grid_y),
|
||||
slice_count: Some(slice_count),
|
||||
slice_warning,
|
||||
prompt,
|
||||
@@ -2332,8 +2410,10 @@ pub async fn split_editor_icon_spritesheet(
|
||||
source,
|
||||
processing_deadline,
|
||||
memory_admission,
|
||||
EditorIconSpritesheetSliceMode::ConnectedComponents,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await?;
|
||||
let prompt = source_resource
|
||||
@@ -2408,8 +2488,10 @@ pub async fn split_editor_icon_spritesheet(
|
||||
pub(crate) async fn slice_editor_icon_spritesheet_all(
|
||||
source: DownloadedImage,
|
||||
request_deadline: Option<Instant>,
|
||||
slice_layout: Option<EditorIconSpritesheetSliceLayout>,
|
||||
slice_mode: EditorIconSpritesheetSliceMode,
|
||||
slice_count: Option<usize>,
|
||||
grid_x: u32,
|
||||
grid_y: u32,
|
||||
) -> Result<PreparedEditorIconSpritesheetPlan, AppError> {
|
||||
let processing_deadline =
|
||||
resolve_editor_icon_spritesheet_processing_deadline(Instant::now(), request_deadline);
|
||||
@@ -2419,8 +2501,10 @@ pub(crate) async fn slice_editor_icon_spritesheet_all(
|
||||
source,
|
||||
processing_deadline,
|
||||
memory_admission,
|
||||
slice_layout,
|
||||
slice_mode,
|
||||
slice_count,
|
||||
grid_x,
|
||||
grid_y,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -2458,8 +2542,10 @@ async fn slice_editor_icon_spritesheet_all_with_memory_admission(
|
||||
source: DownloadedImage,
|
||||
processing_deadline: Instant,
|
||||
memory_admission: Arc<EditorIconSpritesheetMemoryAdmission>,
|
||||
slice_layout: Option<EditorIconSpritesheetSliceLayout>,
|
||||
slice_mode: EditorIconSpritesheetSliceMode,
|
||||
slice_count: Option<usize>,
|
||||
grid_x: u32,
|
||||
grid_y: u32,
|
||||
) -> Result<PreparedEditorIconSpritesheetPlan, AppError> {
|
||||
if Instant::now() >= processing_deadline {
|
||||
return Err(editor_icon_spritesheet_processing_timeout_error());
|
||||
@@ -2492,17 +2578,19 @@ async fn slice_editor_icon_spritesheet_all_with_memory_admission(
|
||||
return Err(editor_icon_spritesheet_processing_timeout_error());
|
||||
}
|
||||
validate_editor_icon_spritesheet_source(&source)?;
|
||||
match slice_layout {
|
||||
Some(EditorIconSpritesheetSliceLayout::Grid2x2) => {
|
||||
prepare_generated_icon_spritesheet_grid_2x2(&source)
|
||||
match slice_mode {
|
||||
EditorIconSpritesheetSliceMode::Grid => {
|
||||
prepare_generated_icon_spritesheet_grid(&source, grid_x, grid_y)
|
||||
}
|
||||
EditorIconSpritesheetSliceMode::ConnectedComponents => {
|
||||
prepare_generated_icon_spritesheet_all_by_connected_components(
|
||||
&source,
|
||||
slice_count
|
||||
.unwrap_or(EDITOR_ICON_SPRITESHEET_MAX_SLICES)
|
||||
.min(EDITOR_ICON_SPRITESHEET_MAX_SLICES),
|
||||
EDITOR_ICON_SPRITESHEET_MAX_TOTAL_CROP_PIXELS,
|
||||
)
|
||||
}
|
||||
None => prepare_generated_icon_spritesheet_all_by_connected_components(
|
||||
&source,
|
||||
slice_count
|
||||
.unwrap_or(EDITOR_ICON_SPRITESHEET_MAX_SLICES)
|
||||
.min(EDITOR_ICON_SPRITESHEET_MAX_SLICES),
|
||||
EDITOR_ICON_SPRITESHEET_MAX_TOTAL_CROP_PIXELS,
|
||||
),
|
||||
}
|
||||
.map_err(map_editor_icon_spritesheet_platform_error)
|
||||
});
|
||||
@@ -2909,7 +2997,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn grid_2x2_slicing_returns_exactly_four_quadrants_with_detached_details() {
|
||||
async fn grid_slicing_returns_one_slice_per_declared_cell_with_detached_details() {
|
||||
use image::{ImageBuffer, ImageFormat, Rgba};
|
||||
|
||||
let mut image: image::RgbaImage = ImageBuffer::from_pixel(128, 128, Rgba([0, 255, 0, 255]));
|
||||
@@ -2939,15 +3027,54 @@ mod tests {
|
||||
let prepared = slice_editor_icon_spritesheet_all(
|
||||
source,
|
||||
None,
|
||||
Some(EditorIconSpritesheetSliceLayout::Grid2x2),
|
||||
EditorIconSpritesheetSliceMode::Grid,
|
||||
None,
|
||||
2,
|
||||
2,
|
||||
)
|
||||
.await
|
||||
.expect("declared 2x2 sheet should slice");
|
||||
.expect("declared grid sheet should slice");
|
||||
|
||||
assert_eq!(prepared.plan.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slice_mode_defaults_to_connected_components_and_accepts_explicit_modes() {
|
||||
assert_eq!(
|
||||
resolve_editor_icon_spritesheet_slice_mode(None),
|
||||
EditorIconSpritesheetSliceMode::ConnectedComponents
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_editor_icon_spritesheet_grid_dimensions(
|
||||
EditorIconSpritesheetSliceMode::Grid,
|
||||
Some(3),
|
||||
Some(2),
|
||||
)
|
||||
.expect("grid dimensions should validate"),
|
||||
(3, 2)
|
||||
);
|
||||
let connected: EditorIconSpritesheetGenerationRequest = serde_json::from_value(json!({
|
||||
"referenceId": "spec",
|
||||
"iconDescriptions": ["素材"],
|
||||
"sliceMode": "connected-components"
|
||||
}))
|
||||
.expect("explicit connected-components mode should deserialize");
|
||||
assert_eq!(
|
||||
connected.slice_mode,
|
||||
Some(EditorIconSpritesheetSliceMode::ConnectedComponents)
|
||||
);
|
||||
let grid: EditorIconSpritesheetGenerationRequest = serde_json::from_value(json!({
|
||||
"referenceId": "spec",
|
||||
"iconDescriptions": ["素材"],
|
||||
"sliceMode": "grid",
|
||||
"gridX": 3,
|
||||
"gridY": 2
|
||||
}))
|
||||
.expect("grid mode should deserialize");
|
||||
assert_eq!(grid.grid_x, Some(3));
|
||||
assert_eq!(grid.grid_y, Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spritesheet_genre_requires_exact_game_type_title() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -2620,6 +2620,14 @@ mod tests {
|
||||
icon_spritesheet_request["properties"]["sliceCount"]["minimum"],
|
||||
json!(1)
|
||||
);
|
||||
assert_eq!(
|
||||
icon_spritesheet_request["properties"]["sliceMode"]["enum"],
|
||||
json!(["connected-components", "grid"])
|
||||
);
|
||||
assert_eq!(
|
||||
icon_spritesheet_request["properties"]["gridX"]["maximum"],
|
||||
json!(32)
|
||||
);
|
||||
let icon_style_schema = &parsed["components"]["schemas"]["EditorIconSpritesheetGenerationRequest"]
|
||||
["properties"]["style"];
|
||||
assert_eq!(icon_style_schema["anyOf"][0]["type"], "string");
|
||||
|
||||
@@ -1364,7 +1364,9 @@ fn compact_external_api_generation_result(result: Value) -> Value {
|
||||
| "spritesheetWidth"
|
||||
| "spritesheetHeight"
|
||||
| "iconImageSrcs"
|
||||
| "sliceLayout"
|
||||
| "sliceMode"
|
||||
| "gridX"
|
||||
| "gridY"
|
||||
| "sliceCount"
|
||||
| "frames"
|
||||
| "frameCount"
|
||||
@@ -2436,7 +2438,9 @@ mod tests {
|
||||
let mut job = external_generation_job_record_fixture(Some("lease-1"));
|
||||
job.dedupe_key = "external-api-generation:conversation-1:7:icon-spritesheet".to_string();
|
||||
let response = json!({
|
||||
"sliceLayout": "grid-2x2",
|
||||
"sliceMode": "grid",
|
||||
"gridX": 2,
|
||||
"gridY": 2,
|
||||
"iconImageSrcs": [
|
||||
{ "name": "素材 1", "imageSrc": "/api/assets/object/one.png" },
|
||||
{ "name": "素材 2", "imageSrc": "/api/assets/object/two.png" },
|
||||
@@ -2449,7 +2453,7 @@ mod tests {
|
||||
serde_json::from_str(&editor_generation_result_payload_json(&job, &response))
|
||||
.expect("worker result should be valid JSON");
|
||||
|
||||
assert_eq!(payload["result"]["sliceLayout"], json!("grid-2x2"));
|
||||
assert_eq!(payload["result"]["sliceMode"], json!("grid"));
|
||||
assert_eq!(
|
||||
payload["result"]["iconImageSrcs"].as_array().map(Vec::len),
|
||||
Some(4)
|
||||
@@ -2705,7 +2709,9 @@ mod tests {
|
||||
"spritesheetImageSrc": "/api/assets/object/core-sheet.png",
|
||||
"spritesheetWidth": 1024,
|
||||
"spritesheetHeight": 1024,
|
||||
"sliceLayout": "grid-2x2",
|
||||
"sliceMode": "grid",
|
||||
"gridX": 2,
|
||||
"gridY": 2,
|
||||
"spritesheetResource": {
|
||||
"resourceId": "sheet-resource-1",
|
||||
"objectKey": "users/user-1/core-sheet.png",
|
||||
@@ -2729,7 +2735,7 @@ mod tests {
|
||||
serde_json::from_str(&editor_generation_result_payload_json(&job, &response))
|
||||
.expect("游戏创作客户端完成结果应持久化为合法 JSON");
|
||||
|
||||
assert_eq!(payload["result"]["sliceLayout"], json!("grid-2x2"));
|
||||
assert_eq!(payload["result"]["sliceMode"], json!("grid"));
|
||||
assert_eq!(
|
||||
payload["result"]["iconImageSrcs"].as_array().map(Vec::len),
|
||||
Some(4)
|
||||
|
||||
@@ -54,7 +54,7 @@ const SKILL_REQUESTS_AND_OUTPUTS_URI: &str =
|
||||
"genarrative://external-editor/skill/references/requests-and-outputs.md";
|
||||
const MAX_MCP_REST_RESPONSE_BYTES: usize = 4 * 1024 * 1024;
|
||||
|
||||
const MCP_INSTRUCTIONS: &str = r#"陶泥儿外部编辑器工具。先创建或复用画布项目,并创建与画布同名的素材文件夹;生成结果应同时写入画布和素材库。参考本地文件时先走上传票据和对象确认,不要把 Data URL、Blob URL 或临时签名 URL写入生成参数。所有生成工具都是异步提交:必须提供 idempotencyKey,提交后按 pollAfterMs 调用 get_external_editor_generation_job,只有 status=completed 时消费 result;查询超时不能重新提交。warning 表示主结果可用但存在降级,sliceWarning 表示完整透明图集可用但切片未完成。详细说明、OpenAPI、Skill 主入口和分主题 references 见 resources/list;需要本地文件编排或不支持 MCP 时再下载 skill.zip。"#;
|
||||
const MCP_INSTRUCTIONS: &str = r#"陶泥儿外部编辑器工具。先创建或复用画布项目,并创建与画布同名的素材文件夹;生成结果应同时写入画布和素材库。参考本地文件时先走上传票据和对象确认,不要把 Data URL、Blob URL 或临时签名 URL写入生成参数。所有生成工具都是异步提交:必须提供 idempotencyKey,提交后按 pollAfterMs 调用 get_external_editor_generation_job,只有 status=completed 时消费 result;查询超时不能重新提交。图集生成可用 sliceMode=connected-components(默认连通域切分)或 grid(必须同时提供 gridX/gridY)。warning 表示主结果可用但存在降级,sliceWarning 表示完整透明图集可用但切片未完成。详细说明、OpenAPI、Skill 主入口和分主题 references 见 resources/list;需要本地文件编排或不支持 MCP 时再下载 skill.zip。"#;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct McpOperation {
|
||||
|
||||
@@ -276,13 +276,17 @@ pub(crate) fn build_spritesheet_prompt(
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_grid_2x2_spritesheet_prompt(
|
||||
pub(crate) fn build_grid_spritesheet_prompt(
|
||||
user_prompt: &ValidatedEditorIconSpritesheetPrompt,
|
||||
screen_color: EditorScreenBackgroundColor,
|
||||
genre: Option<GameGenre>,
|
||||
columns: u32,
|
||||
rows: u32,
|
||||
) -> String {
|
||||
format!(
|
||||
"{}\n\n固定 2×2 游戏核心素材图集合同:画面必须严格分为左上、右上、左下、右下四个等大的独立槽位;每个槽位只放一个完整、可单独用于游戏运行时的主体。四个槽位必须按用户给出的四条素材需求顺序对应,且每格都必须有清晰可见的主体。禁止生成任何额外图标、同一主体的多个姿势、序列帧、棋盘、场景、边框、流程箭头、标签、文字、Logo、装饰小物或第五个素材;禁止主体跨格、触碰或重叠。输出须是单张图集,背景只使用统一纯色以便透明化。",
|
||||
"{}\n\n固定网格游戏核心素材图集合同:画面必须严格分为 {}×{} 个等大的独立槽位;每个槽位只放一个完整、可单独用于游戏运行时的主体。素材按从左到右、从上到下顺序对应,禁止主体跨格、触碰或重叠。输出须是单张图集,背景只使用统一纯色以便透明化。",
|
||||
columns,
|
||||
rows,
|
||||
build_spritesheet_prompt(user_prompt, screen_color, genre),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ pub use sheet::{
|
||||
GeneratedAssetSheetSliceImage, crop_generated_asset_sheet_view_edge_matte,
|
||||
crop_generated_asset_sheet_view_edge_matte_with_options,
|
||||
prepare_generated_icon_spritesheet_all_by_connected_components,
|
||||
prepare_generated_icon_spritesheet_grid_2x2, slice_generated_asset_sheet,
|
||||
prepare_generated_icon_spritesheet_grid, slice_generated_asset_sheet,
|
||||
slice_generated_asset_sheet_two_items_per_row,
|
||||
slice_generated_icon_spritesheet_all_by_connected_components,
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user