补齐游戏场景后端集成契约
归一空白场景生成选项 接入场景权威画布素材类型 统一完美像素迁移哈希口径 补充场景后端回归测试
This commit is contained in:
@@ -1718,6 +1718,28 @@ fn build_editor_scene_generation_inputs(
|
||||
json!({ "fields": fields, "references": references })
|
||||
}
|
||||
|
||||
fn normalize_editor_scene_optional_text<'a>(value: Option<&'a str>, default: &'a str) -> &'a str {
|
||||
value
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn normalize_editor_scene_generation_options(
|
||||
model: Option<&str>,
|
||||
aspect_ratio: Option<&str>,
|
||||
image_size: Option<&str>,
|
||||
) -> EditorGenerationOptions {
|
||||
normalize_editor_generation_options(
|
||||
Some(normalize_editor_scene_optional_text(
|
||||
model,
|
||||
EDITOR_IMAGE_MODEL_NANOBANANA2,
|
||||
)),
|
||||
Some(normalize_editor_scene_optional_text(aspect_ratio, "16:9")),
|
||||
Some(normalize_editor_scene_optional_text(image_size, "1K")),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn generate_editor_scene(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -1725,13 +1747,10 @@ pub async fn generate_editor_scene(
|
||||
payload: Result<Json<EditorSceneGenerateRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let Json(payload) = parse_editor_generation_json_payload(payload)?;
|
||||
let generation_options = normalize_editor_generation_options(
|
||||
payload
|
||||
.model
|
||||
.as_deref()
|
||||
.or(Some(EDITOR_IMAGE_MODEL_NANOBANANA2)),
|
||||
payload.aspect_ratio.as_deref().or(Some("16:9")),
|
||||
payload.image_size.as_deref().or(Some("1K")),
|
||||
let generation_options = normalize_editor_scene_generation_options(
|
||||
payload.model.as_deref(),
|
||||
payload.aspect_ratio.as_deref(),
|
||||
payload.image_size.as_deref(),
|
||||
);
|
||||
let prompt = build_editor_scene_prompt(
|
||||
payload.scene_content.as_str(),
|
||||
@@ -18286,6 +18305,32 @@ mod tests {
|
||||
assert_eq!(profile.slot, "scene");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_generation_blank_options_match_omitted_defaults() {
|
||||
let omitted = normalize_editor_scene_generation_options(None, None, None);
|
||||
let blank =
|
||||
normalize_editor_scene_generation_options(Some(" "), Some(" \t "), Some("\n "));
|
||||
let explicit = normalize_editor_scene_generation_options(
|
||||
Some(EDITOR_IMAGE_MODEL_NANOBANANA2),
|
||||
Some("16:9"),
|
||||
Some("1K"),
|
||||
);
|
||||
|
||||
assert_eq!(omitted, explicit);
|
||||
assert_eq!(blank, explicit);
|
||||
assert_eq!(explicit.model, EDITOR_IMAGE_MODEL_NANOBANANA2);
|
||||
assert_eq!(explicit.aspect_ratio, "16:9");
|
||||
assert_eq!(explicit.image_size, "1K");
|
||||
assert_eq!(
|
||||
crate::editor_generation_config::editor_image_generation_mud_points(
|
||||
Some("scene"),
|
||||
Some(explicit.model),
|
||||
Some(explicit.image_size),
|
||||
),
|
||||
12,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_generation_rebuilds_fields_and_only_keeps_reference_rows() {
|
||||
let payload: EditorSceneGenerateRequest = serde_json::from_value(json!({
|
||||
|
||||
@@ -22,10 +22,9 @@ use crate::{
|
||||
generate_editor_scene, get_editor_asset_library, get_editor_generation_pricing,
|
||||
get_editor_project, list_editor_projects, list_public_editor_project_resources,
|
||||
load_recent_editor_project, remove_editor_image_background, rename_editor_project,
|
||||
save_editor_project_layout, snap_editor_image_to_pixel_art,
|
||||
split_editor_icon_spritesheet, submit_editor_asset_showcase,
|
||||
toggle_editor_showcase_asset_like, update_editor_asset, update_editor_asset_folder,
|
||||
update_editor_project_resource_showcase,
|
||||
save_editor_project_layout, snap_editor_image_to_pixel_art, split_editor_icon_spritesheet,
|
||||
submit_editor_asset_showcase, toggle_editor_showcase_asset_like, update_editor_asset,
|
||||
update_editor_asset_folder, update_editor_project_resource_showcase,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
@@ -32,8 +32,9 @@ const EDITOR_CANVAS_LAYER_RESOURCE_METADATA_FIELDS: [&str; 12] = [
|
||||
"assetKind",
|
||||
"generationInputs",
|
||||
];
|
||||
const EDITOR_CANVAS_ASSET_KINDS: [&str; 11] = [
|
||||
const EDITOR_CANVAS_ASSET_KINDS: [&str; 12] = [
|
||||
"spec",
|
||||
"scene",
|
||||
"character",
|
||||
"character-animation",
|
||||
"icon",
|
||||
@@ -8828,14 +8829,13 @@ fn validate_editor_pixel_art_planned_canvas_layout(
|
||||
.filter(|migration| migration.status == EDITOR_CANVAS_LAYOUT_MIGRATION_STATUS_ACTIVE)
|
||||
.ok_or_else(|| "结构化图片画布缺少 active 迁移记录,拒绝保存".to_string())?;
|
||||
let current_structured_json = build_structured_canvas_layout_json(ctx, canvas)?;
|
||||
let current_structured_hash = canonical_layout_sha256(current_structured_json.as_str())?;
|
||||
let current_structured_integrity =
|
||||
canvas_layout_integrity(current_structured_json.as_str())?;
|
||||
verify_migration_layout(
|
||||
let current_resource_asset_kinds =
|
||||
project_resource_asset_kinds(ctx, project_id, owner_user_id);
|
||||
verify_active_structured_canvas_migration(
|
||||
&active_migration,
|
||||
canvas.revision,
|
||||
current_structured_hash.as_str(),
|
||||
¤t_structured_integrity,
|
||||
current_structured_json.as_str(),
|
||||
¤t_resource_asset_kinds,
|
||||
)?;
|
||||
}
|
||||
normalize_layout_json(layers_json).and_then(|layers_json| {
|
||||
@@ -8850,6 +8850,18 @@ fn validate_editor_pixel_art_planned_canvas_layout(
|
||||
})
|
||||
}
|
||||
|
||||
fn verify_active_structured_canvas_migration(
|
||||
migration: &EditorCanvasLayoutMigration,
|
||||
revision: u64,
|
||||
structured_json: &str,
|
||||
resource_asset_kinds: &BTreeMap<String, Option<String>>,
|
||||
) -> Result<(), String> {
|
||||
let structured_hash =
|
||||
canonical_layout_sha256_with_resource_asset_kinds(structured_json, resource_asset_kinds)?;
|
||||
let integrity = canvas_layout_integrity(structured_json)?;
|
||||
verify_migration_layout(migration, revision, structured_hash.as_str(), &integrity)
|
||||
}
|
||||
|
||||
fn backfill_editor_canvas_layout(
|
||||
ctx: &ReducerContext,
|
||||
input: EditorCanvasLayoutMigrationInput,
|
||||
@@ -10000,6 +10012,48 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_art_canvas_completion_accepts_scene_asset_kind() {
|
||||
let layout = json!([{
|
||||
"itemType": "generation-dialog",
|
||||
"layerId": "generation-dialog:dialog-1",
|
||||
"resourceId": "generation-dialog:dialog-1",
|
||||
"dialog": {
|
||||
"id": "dialog-1",
|
||||
"mode": "quick-edit",
|
||||
"status": "generating",
|
||||
"placeholder": {
|
||||
"x": 100,
|
||||
"y": 50,
|
||||
"width": 200,
|
||||
"height": 100,
|
||||
"originalWidth": 200,
|
||||
"originalHeight": 100
|
||||
}
|
||||
}
|
||||
}]);
|
||||
let mut resource = pixel_art_project_resource();
|
||||
resource.asset_kind = Some("scene".to_string());
|
||||
|
||||
let applied = match plan_editor_pixel_art_canvas_layout(
|
||||
layout.to_string().as_str(),
|
||||
&pixel_art_canvas_completion(),
|
||||
&resource,
|
||||
)
|
||||
.expect("scene pixel-art completion should apply")
|
||||
{
|
||||
EditorPixelArtCanvasLayoutPlan::Apply(layers_json) => layers_json,
|
||||
_ => panic!("scene completion must apply"),
|
||||
};
|
||||
let parsed = parse_structured_canvas_layout(applied.as_str())
|
||||
.expect("scene result layer should be accepted by structured layout parsing");
|
||||
let extension: JsonValue = serde_json::from_str(&parsed.layers[0].item_json)
|
||||
.expect("scene result layer extension");
|
||||
|
||||
assert_eq!(parsed.layers.len(), 1);
|
||||
assert_eq!(extension["assetKind"], json!("scene"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_art_canvas_completion_keeps_deleted_dialog_absent() {
|
||||
let layout = json!([{
|
||||
@@ -11578,6 +11632,77 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structured_canvas_scene_resource_kind_survives_canonical_normalization() {
|
||||
let layout = r#"[{"layerId":"layer-1","resourceId":"resource-scene"}]"#;
|
||||
let resource_asset_kinds =
|
||||
BTreeMap::from([("resource-scene".to_string(), Some("scene".to_string()))]);
|
||||
|
||||
let canonical =
|
||||
canonical_layout_json_with_resource_asset_kinds(layout, &resource_asset_kinds)
|
||||
.expect("scene resource kind should canonicalize");
|
||||
let parsed = parse_structured_canvas_layout(canonical.as_str())
|
||||
.expect("canonical scene layout should remain structurally valid");
|
||||
let rebuilt = serialize_structured_canvas_layout(&parsed)
|
||||
.expect("canonical scene layout should persist structurally");
|
||||
let rebuilt: JsonValue = serde_json::from_str(&rebuilt).expect("rebuilt scene layout");
|
||||
|
||||
assert_eq!(
|
||||
parsed.layers[0].asset_kind_override.as_deref(),
|
||||
Some("scene")
|
||||
);
|
||||
assert_eq!(rebuilt[0]["assetKindOverride"], json!("scene"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_migration_verification_uses_scene_resource_kind() {
|
||||
let now = Timestamp::from_micros_since_unix_epoch(1_000_000);
|
||||
let structured_json = r#"[{"layerId":"layer-1","resourceId":"resource-scene"}]"#;
|
||||
let resource_asset_kinds =
|
||||
BTreeMap::from([("resource-scene".to_string(), Some("scene".to_string()))]);
|
||||
let structured_hash = canonical_layout_sha256_with_resource_asset_kinds(
|
||||
structured_json,
|
||||
&resource_asset_kinds,
|
||||
)
|
||||
.expect("scene-aware migration hash");
|
||||
let integrity = canvas_layout_integrity(structured_json).expect("scene layout integrity");
|
||||
let migration = EditorCanvasLayoutMigration {
|
||||
canvas_id: "canvas-1".to_string(),
|
||||
project_id: "project-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
source_layout_sha256: "source".to_string(),
|
||||
structured_layout_sha256: structured_hash,
|
||||
verified_revision: 7,
|
||||
layer_count: integrity.layer_count,
|
||||
dialog_count: integrity.dialog_count,
|
||||
resource_refs_sha256: integrity.resource_refs_sha256,
|
||||
migration_version: EDITOR_CANVAS_LAYOUT_STORAGE_VERSION_STRUCTURED,
|
||||
status: EDITOR_CANVAS_LAYOUT_MIGRATION_STATUS_ACTIVE.to_string(),
|
||||
migrated_at: now,
|
||||
activated_at: Some(now),
|
||||
rolled_back_at: None,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
verify_active_structured_canvas_migration(
|
||||
&migration,
|
||||
7,
|
||||
structured_json,
|
||||
&resource_asset_kinds,
|
||||
)
|
||||
.expect("scene resource kind should match the active migration hash");
|
||||
assert!(
|
||||
verify_active_structured_canvas_migration(
|
||||
&migration,
|
||||
7,
|
||||
structured_json,
|
||||
&BTreeMap::new(),
|
||||
)
|
||||
.is_err(),
|
||||
"dropping the scene resource kind must still be detected as drift"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structured_canvas_allows_distinct_layers_to_share_one_resource() {
|
||||
let shared = r#"[
|
||||
|
||||
Reference in New Issue
Block a user