From 3bea6441a8018d02a9184b93f863b2c20b3da186 Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 4 Aug 2026 06:53:29 +0000 Subject: [PATCH] =?UTF-8?q?=E6=B8=85=E7=90=86=E5=AE=8C=E7=BE=8E=E5=83=8F?= =?UTF-8?q?=E7=B4=A0=E9=93=BE=E8=B7=AF=E7=9A=84=E6=AD=BB=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除只有测试调用的 snap_pixel_art_strict 与 snap_pixel_art 两个无 deadline 包装,公开入口收敛为 snap_pixel_art_with_deadline 与 snap_pixel_art_strict_with_deadline;原本只写在 snap_pixel_art 头上的双输入 尺寸与输出契约合并进保留者的文档,调用点改为显式传 None。 - 去掉持久化结果里从不被读取的 status 透传(客户端枚举与 record 字段)。该段 原本搭载了「status 缺失即拒绝」的校验,保留为显式 is_none 拦截,并补一个 此前缺失的回归用例锁住它。 - 快速编辑面板不再对 dialog 状态做 pending-confirmation 的运行时收窄:dialog 由面板派生,写回方只做 failed→idle 重置,该分支恒不成立,直接取面板状态。 - 收回三个无人导入的 export(reconcilePerfectPixelProject、 createPerfectPixelReconciliationOperation、 INVALID_PERFECT_PIXEL_OPERATION_ERROR_MESSAGE)。 Co-Authored-By: Claude Opus 5 --- server-rs/crates/platform-image/src/lib.rs | 4 +- .../platform-image/src/pixel_art_snapper.rs | 49 ++++++-------- .../spacetime-client/src/editor_project.rs | 67 ++++++++----------- .../image-editor/ImageCanvasEditorModel.ts | 2 +- .../ImageCanvasQuickEditPanelView.tsx | 9 +-- .../useImageCanvasGenerationWorkflow.ts | 4 +- 6 files changed, 58 insertions(+), 77 deletions(-) diff --git a/server-rs/crates/platform-image/src/lib.rs b/server-rs/crates/platform-image/src/lib.rs index b2bf21a2e..8db6a9c97 100644 --- a/server-rs/crates/platform-image/src/lib.rs +++ b/server-rs/crates/platform-image/src/lib.rs @@ -5,8 +5,8 @@ pub mod vector_engine; pub use pixel_art_snapper::{ PIXEL_ART_ALPHA_COVERAGE_THRESHOLD, PIXEL_ART_ANALYSIS_COLORS, PIXEL_ART_KMEANS_SAMPLE_LIMIT, - PIXEL_ART_MAX_IMAGE_PIXELS, PixelArtSnapError, snap_pixel_art, snap_pixel_art_strict, - snap_pixel_art_strict_with_deadline, snap_pixel_art_with_deadline, + PIXEL_ART_MAX_IMAGE_PIXELS, PixelArtSnapError, snap_pixel_art_strict_with_deadline, + snap_pixel_art_with_deadline, }; pub use vector_engine::{ DownloadedImage, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, GeneratedImages, NANOBANANA_2_MODEL, diff --git a/server-rs/crates/platform-image/src/pixel_art_snapper.rs b/server-rs/crates/platform-image/src/pixel_art_snapper.rs index 499cb0325..8e46a169d 100644 --- a/server-rs/crates/platform-image/src/pixel_art_snapper.rs +++ b/server-rs/crates/platform-image/src/pixel_art_snapper.rs @@ -156,31 +156,11 @@ impl SnapConfig { /// image is always a PNG at the original `rgba_source` dimensions. Its alpha /// channel contains only `0` or `255`, and fully transparent pixels are /// canonical `[0, 0, 0, 0]`. -pub fn snap_pixel_art( - grid_source: &DownloadedImage, - rgba_source: &DownloadedImage, -) -> Result { - snap_pixel_art_with_deadline(grid_source, rgba_source, None) -} - -/// Snap an image unless legacy analysis detects no grid step on either axis. -/// -/// Unlike [`snap_pixel_art`], this entry does not synthesize a uniform -/// min-dimension/64 grid when neither axis contains a detectable step. Every -/// other detection, walking, sampling, and encoding behavior is identical. -pub fn snap_pixel_art_strict( - grid_source: &DownloadedImage, - rgba_source: &DownloadedImage, -) -> Result { - snap_pixel_art_strict_with_deadline(grid_source, rgba_source, None) -} - -/// Deadline-aware variant of [`snap_pixel_art`]. /// /// The deadline is checked before and after non-cooperative codec/resize /// operations, and periodically inside the K-means, profile, and cell-sampling /// loops. Exceeding it returns [`PixelArtSnapError::DeadlineExceeded`] without -/// producing a partial image. +/// producing a partial image. Pass `None` to opt out of deadline checks. pub fn snap_pixel_art_with_deadline( grid_source: &DownloadedImage, rgba_source: &DownloadedImage, @@ -189,7 +169,12 @@ pub fn snap_pixel_art_with_deadline( snap_pixel_art_with_grid_policy(grid_source, rgba_source, deadline, false) } -/// Deadline-aware variant of [`snap_pixel_art_strict`]. +/// Snap an image unless legacy analysis detects no grid step on either axis. +/// +/// Unlike [`snap_pixel_art_with_deadline`], this entry does not synthesize a +/// uniform min-dimension/64 grid when neither axis contains a detectable step. +/// Every other detection, walking, sampling, and encoding behavior — including +/// the deadline semantics — is identical. pub fn snap_pixel_art_strict_with_deadline( grid_source: &DownloadedImage, rgba_source: &DownloadedImage, @@ -1111,7 +1096,8 @@ mod tests { let grid = downloaded_png(RgbaImage::from_pixel(8, 8, Rgba([10, 20, 30, 255]))); let rgba = downloaded_png(RgbaImage::from_pixel(8, 9, Rgba([10, 20, 30, 255]))); - let error = snap_pixel_art(&grid, &rgba).expect_err("dimensions should mismatch"); + let error = snap_pixel_art_with_deadline(&grid, &rgba, None) + .expect_err("dimensions should mismatch"); assert!( error .to_string() @@ -1141,9 +1127,9 @@ mod tests { fn strict_mode_rejects_only_when_legacy_would_use_uniform_fallback() { let source = downloaded_png(RgbaImage::from_pixel(128, 128, Rgba([10, 20, 30, 255]))); - let legacy = snap_pixel_art(&source, &source) + let legacy = snap_pixel_art_with_deadline(&source, &source, None) .expect("legacy generation style should retain its uniform fallback"); - let strict = snap_pixel_art_strict(&source, &source) + let strict = snap_pixel_art_strict_with_deadline(&source, &source, None) .expect_err("explicit strict action should reject an undetected grid"); assert_eq!(decode_output(&legacy).dimensions(), (128, 128)); @@ -1179,9 +1165,9 @@ mod tests { ); let source = downloaded_png(image); - let legacy = snap_pixel_art(&source, &source) + let legacy = snap_pixel_art_with_deadline(&source, &source, None) .expect("legacy processing should succeed with a detected step"); - let strict = snap_pixel_art_strict(&source, &source) + let strict = snap_pixel_art_strict_with_deadline(&source, &source, None) .expect("strict processing should reuse the detected legacy step"); assert_eq!(strict.bytes, legacy.bytes); assert_eq!(strict.mime_type, legacy.mime_type); @@ -1208,9 +1194,10 @@ mod tests { } } - let output = snap_pixel_art( + let output = snap_pixel_art_with_deadline( &downloaded_png(grid_image), &downloaded_png(rgba_image.clone()), + None, ) .expect("pixel snapping should succeed"); let decoded = decode_output(&output); @@ -1237,8 +1224,10 @@ mod tests { } let rgba = downloaded_png(rgba); - let first = snap_pixel_art(&grid, &rgba).expect("first snap should succeed"); - let second = snap_pixel_art(&grid, &rgba).expect("second snap should succeed"); + let first = + snap_pixel_art_with_deadline(&grid, &rgba, None).expect("first snap should succeed"); + let second = + snap_pixel_art_with_deadline(&grid, &rgba, None).expect("second snap should succeed"); assert_eq!(first.bytes, second.bytes); for pixel in decode_output(&first).pixels() { diff --git a/server-rs/crates/spacetime-client/src/editor_project.rs b/server-rs/crates/spacetime-client/src/editor_project.rs index d987fe519..6da9a4179 100644 --- a/server-rs/crates/spacetime-client/src/editor_project.rs +++ b/server-rs/crates/spacetime-client/src/editor_project.rs @@ -30,16 +30,8 @@ pub struct EditorPixelArtResultPersistRecordInput { pub completed_at_micros: i64, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum EditorPixelArtResultPersistRecordStatus { - Applied, - DialogMissing, - AlreadyApplied, -} - #[derive(Clone, Debug, PartialEq)] pub struct EditorPixelArtResultPersistRecord { - pub status: EditorPixelArtResultPersistRecordStatus, pub asset_object: module_assets::AssetObjectUpsertSnapshot, pub project_resource: EditorProjectResourceRecord, pub asset: EditorAssetRecord, @@ -1103,20 +1095,13 @@ fn map_editor_pixel_art_result_persist_result( if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let status = match result - .status - .ok_or_else(|| SpacetimeClientError::validation_failed("完美像素持久化结果缺少状态"))? - { - crate::module_bindings::EditorPixelArtResultPersistStatus::Applied => { - EditorPixelArtResultPersistRecordStatus::Applied - } - crate::module_bindings::EditorPixelArtResultPersistStatus::DialogMissing => { - EditorPixelArtResultPersistRecordStatus::DialogMissing - } - crate::module_bindings::EditorPixelArtResultPersistStatus::AlreadyApplied => { - EditorPixelArtResultPersistRecordStatus::AlreadyApplied - } - }; + // 中文注释:三种落库状态(Applied / DialogMissing / AlreadyApplied)目前没有任何 + // 上层消费方,不再向上透传;但状态缺失说明 procedure 返回体不完整,仍要拦在这里。 + if result.status.is_none() { + return Err(SpacetimeClientError::validation_failed( + "完美像素持久化结果缺少状态", + )); + } let asset_object = result.asset_object.ok_or_else(|| { SpacetimeClientError::validation_failed("完美像素持久化结果缺少 asset object") })?; @@ -1141,7 +1126,6 @@ fn map_editor_pixel_art_result_persist_result( .flatten(); Ok(EditorPixelArtResultPersistRecord { - status, asset_object: map_editor_spritesheet_asset_object_snapshot(asset_object), project_resource: map_editor_spritesheet_project_resource_snapshot(project_resource)?, asset: map_editor_spritesheet_asset_snapshot(asset)?, @@ -1422,29 +1406,36 @@ mod pixel_art_persist_mapper_tests { } #[test] - fn editor_pixel_art_persist_mapper_preserves_status_and_accepts_missing_project() { + fn editor_pixel_art_persist_mapper_rejects_missing_status() { + let mut result = successful_pixel_art_persist_result( + crate::module_bindings::EditorPixelArtResultPersistStatus::Applied, + ); + result.status = None; + + let error = map_editor_pixel_art_result_persist_result(result) + .expect_err("成功结果缺少状态时必须拒绝"); + + assert!(matches!( + error, + SpacetimeClientError::Runtime(message) + if message == "完美像素持久化结果缺少状态" + )); + } + + #[test] + fn editor_pixel_art_persist_mapper_accepts_every_status_and_missing_project() { let cases = [ - ( - crate::module_bindings::EditorPixelArtResultPersistStatus::Applied, - EditorPixelArtResultPersistRecordStatus::Applied, - ), - ( - crate::module_bindings::EditorPixelArtResultPersistStatus::DialogMissing, - EditorPixelArtResultPersistRecordStatus::DialogMissing, - ), - ( - crate::module_bindings::EditorPixelArtResultPersistStatus::AlreadyApplied, - EditorPixelArtResultPersistRecordStatus::AlreadyApplied, - ), + crate::module_bindings::EditorPixelArtResultPersistStatus::Applied, + crate::module_bindings::EditorPixelArtResultPersistStatus::DialogMissing, + crate::module_bindings::EditorPixelArtResultPersistStatus::AlreadyApplied, ]; - for (binding_status, expected_status) in cases { + for binding_status in cases { let mapped = map_editor_pixel_art_result_persist_result( successful_pixel_art_persist_result(binding_status), ) .expect("project=None 仍应映射成功"); - assert_eq!(mapped.status, expected_status); assert_eq!(mapped.asset_object.asset_object_id, "assetobj_pixel"); assert_eq!(mapped.project_resource.resource_id, "editor-resource-pixel"); assert_eq!(mapped.asset.asset_id, "editor-asset-pixel"); diff --git a/src/components/image-editor/ImageCanvasEditorModel.ts b/src/components/image-editor/ImageCanvasEditorModel.ts index d693e20d0..bd109e2f9 100644 --- a/src/components/image-editor/ImageCanvasEditorModel.ts +++ b/src/components/image-editor/ImageCanvasEditorModel.ts @@ -342,7 +342,7 @@ export const PERFECT_PIXEL_RECONCILIATION_WINDOW_MS = 75_000; // 中文注释:第一批曾把 v1 快照写成 240 秒。滚动部署与旧标签页仍可能持久化该形状, // 所以读取侧保留兼容上限;它只决定快照是否可信,不会延长当前 75 秒对账窗口。 const LEGACY_PERFECT_PIXEL_RECONCILIATION_WINDOW_MS = 240_000; -export const INVALID_PERFECT_PIXEL_OPERATION_ERROR_MESSAGE = +const INVALID_PERFECT_PIXEL_OPERATION_ERROR_MESSAGE = '完美像素操作快照无效,禁止自动重试。'; const PERFECT_PIXEL_OPERATION_KEYS = new Set([ diff --git a/src/components/image-editor/ImageCanvasQuickEditPanelView.tsx b/src/components/image-editor/ImageCanvasQuickEditPanelView.tsx index 452b59125..2b200d0c9 100644 --- a/src/components/image-editor/ImageCanvasQuickEditPanelView.tsx +++ b/src/components/image-editor/ImageCanvasQuickEditPanelView.tsx @@ -78,10 +78,11 @@ function syncPanelFromDialogUpdate( model: normalizedDialog.imageModel ?? normalizedPanel.model, quickEditReferences: normalizedDialog.generationReferences, assetLabel: normalizedDialog.assetLabel, - status: - normalizedDialog.status === 'pending-confirmation' - ? normalizedPanel.status - : normalizedDialog.status, + // 中文注释:dialog 由 createQuickEditDialog 从面板自身派生,组合器只会做 + // failed→idle 的重置,不会写入 'pending-confirmation' 这类完美像素专属状态。 + // 因此归一化后的 dialog 状态恒等于归一化后的面板状态,直接取面板侧即可, + // 也让面板的三态联合类型不必在这里做运行时收窄。 + status: normalizedPanel.status, errorMessage: normalizedDialog.errorMessage, }; } diff --git a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts index 6f9ba8022..1cd03bbb2 100644 --- a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts +++ b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts @@ -325,7 +325,7 @@ export function inspectPerfectPixelProjectSnapshot( return { kind: 'applied', project, resource }; } -export function createPerfectPixelReconciliationOperation( +function createPerfectPixelReconciliationOperation( operation: PerfectPixelOperationSnapshot, startedAt = Date.now(), ): PerfectPixelOperationSnapshot { @@ -362,7 +362,7 @@ function waitForPerfectPixelReconciliationDelay( }); } -export async function reconcilePerfectPixelProject( +async function reconcilePerfectPixelProject( projectId: string, operation: PerfectPixelOperationSnapshot, options: { signal?: AbortSignal } = {},