WIP: UI编辑器自动分图层切图标 #304

Draft
k88936 wants to merge 100 commits from feat/ui-editor-auto-seperation into master
5 changed files with 90 additions and 17 deletions
Showing only changes of commit 615d1a28cd - Show all commits
@@ -337,6 +337,30 @@ async fn separate_ui(
ui_editor::commands::separate_ui_impl(project_path, asset_id, state).await
}
#[tauri::command]
fn inspect_separation_recovery(
project_path: String,
asset_id: String,
) -> Result<ui_editor::commands::SeparationRecoveryDTO, String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "asset.list")?;
ui_editor::commands::separation::inspect_separation_recovery(root, &asset_id)
}
#[tauri::command]
fn finalize_separation(project_path: String, asset_id: String) -> Result<(), String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "asset.register")?;
ui_editor::commands::separation::finalize_separation(root, &asset_id)
}
#[tauri::command]
fn discard_separation_recovery(project_path: String, asset_id: String) -> Result<(), String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "asset.register")?;
ui_editor::commands::separation::finalize_separation(root, &asset_id)
}
#[tauri::command]
async fn merge_ui(state: ui_editor::state::State) -> Result<ui_editor::commands::MergeDTO, String> {
ui_editor::commands::merge_ui_impl(state).await
@@ -2569,6 +2593,9 @@ fn main() {
suggest_ui_design_semantic,
recognize_ui,
separate_ui,
inspect_separation_recovery,
finalize_separation,
discard_separation_recovery,
merge_ui,
bind_components,
load_ui_design_state,
@@ -12,6 +12,6 @@ pub(crate) use merge::{merge_ui_impl, merge_ui_impl_with_provider};
pub use recognition::RecognitionDTO;
pub(crate) use recognition::{recognize_ui_impl, recognize_ui_impl_with_provider};
pub(crate) use separation::separate_ui_impl;
pub use separation::SeparationDTO;
pub use separation::{SeparationDTO, SeparationRecoveryDTO};
pub(crate) use ui_design_suggestion::suggest_ui_design_semantic_impl;
pub use ui_design_suggestion::UIDesignSuggestionTreeNode;
@@ -84,6 +84,15 @@ pub struct SeparationDTO {
pub problematic_nodes: Vec<ProblematicNode>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/ui-editor/types/"))]
pub struct SeparationRecoveryDTO {
pub exists: bool,
pub bound_node_count: usize,
pub problematic_node_count: usize,
pub has_pending_tree: bool,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct BindingArea {
@@ -25,6 +25,21 @@ pub fn separation_sidecar_dir(root: &Path, asset_id: &str) -> Result<PathBuf, St
Ok(dir)
}
pub fn separation_state_path(root: &Path, asset_id: &str) -> Result<PathBuf, String> {
Ok(separation_sidecar_dir(root, asset_id)?.join("state.json"))
}
pub fn project_relative_path(root: &Path, path: &Path) -> Result<String, String> {
let relative = path
.strip_prefix(root)
.map_err(|_| "separation 产物必须位于项目目录内".to_string())?;
let value = relative.to_string_lossy().replace('\\', "/");
if value.is_empty() || value.starts_with('/') || value.split('/').any(|part| part == "..") {
return Err("separation 产物相对路径无效".to_string());
}
Ok(value)
}
pub fn write_separation_state(path: &Path, state: &SeparationState) -> Result<(), String> {
if state.schema_version != SEPARATION_STATE_SCHEMA_VERSION {
app_log!("ui_separation.error stage=state_write reason=schema_mismatch");
@@ -116,3 +131,36 @@ pub fn separation_dto(state: &SeparationState) -> SeparationDTO {
problematic_nodes: state.problematic_nodes.clone(),
}
}
pub fn inspect_separation_recovery(
root: &Path,
asset_id: &str,
) -> Result<SeparationRecoveryDTO, String> {
let state_path = separation_state_path(root, asset_id)?;
if !state_path.exists() {
return Ok(SeparationRecoveryDTO {
exists: false,
bound_node_count: 0,
problematic_node_count: 0,
has_pending_tree: false,
});
}
let state = read_separation_state(&state_path)?;
Ok(SeparationRecoveryDTO {
exists: true,
bound_node_count: state.bound.len(),
problematic_node_count: state.problematic_nodes.len(),
has_pending_tree: state.trees.iter().any(|tree| {
!tree.root.children.is_empty() || tree.root_extractable
}),
})
}
pub fn finalize_separation(root: &Path, asset_id: &str) -> Result<(), String> {
let state_path = separation_state_path(root, asset_id)?;
match fs::remove_file(&state_path) {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(format!("删除 separation state 失败:{error}")),
}
}
@@ -367,7 +367,7 @@ pub(crate) async fn separate_ui_impl(
);
format!("创建 separation sidecar 失败:{e}")
})?;
let state_path = sidecar.join("state.json");
let state_path = separation_state_path(root, &asset_id)?;
let restored = state_path.exists();
let mut separation = if restored {
app_log!("ui_separation.state_restore.start asset_id={}", asset_id);
@@ -538,8 +538,10 @@ pub(crate) async fn separate_ui_impl(
.await
{
Ok(()) => {
cut_paths
.insert(to_node.clone(), cut_path.to_string_lossy().to_string());
cut_paths.insert(
to_node.clone(),
project_relative_path(root, &cut_path)?,
);
}
Err(error) => {
app_log!(
@@ -577,19 +579,6 @@ pub(crate) async fn separate_ui_impl(
batch_index += 1;
}
}
match fs::remove_file(&state_path) {
Ok(()) => app_log!("ui_separation.state_removed asset_id={}", asset_id),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
app_log!(
"ui_separation.state_remove_skipped asset_id={} reason=not_found",
asset_id
)
}
Err(error) => app_log!(
"ui_separation.error stage=state_remove asset_id={} error={error}",
asset_id
),
}
app_log!(
"ui_separation.completed asset_id={} bound_nodes={} problematic_nodes={}",
asset_id,