From 56be007ec998d7e47b47b1d0b29a6316e71e5e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 31 Aug 2026 19:07:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=B8=B4=E6=97=B6=20UI=20HTM?= =?UTF-8?q?L=20=E6=B5=8B=E8=AF=95=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 ui-design-to-html standalone bin 保留 Runtime ui.design.to_html 作为唯一 HTML 生成入口 --- .../src-tauri/src/bin/ui-design-to-html.rs | 399 ------------------ 1 file changed, 399 deletions(-) delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/bin/ui-design-to-html.rs diff --git a/apps/ai-game-creator-shell/src-tauri/src/bin/ui-design-to-html.rs b/apps/ai-game-creator-shell/src-tauri/src/bin/ui-design-to-html.rs deleted file mode 100644 index 4bd2b2766..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/bin/ui-design-to-html.rs +++ /dev/null @@ -1,399 +0,0 @@ -use maud::{html, PreEscaped}; -use serde_json::Value; -use std::env; -use std::fmt::Write as _; -use std::fs; - -fn main() { - let args = env::args().skip(1).collect::>(); - if args.len() != 2 { - eprintln!("用法:ui-design-to-html "); - std::process::exit(2); - } - let input = fs::read_to_string(&args[0]) - .unwrap_or_else(|error| fail(&format!("读取输入失败:{error}"))); - let document: Value = serde_json::from_str(&input) - .unwrap_or_else(|error| fail(&format!("解析 UI JSON 失败:{error}"))); - let state = document.get("state").unwrap_or(&document); - let html = render_state(state).unwrap_or_else(|error| fail(&error)); - fs::write(&args[1], html).unwrap_or_else(|error| fail(&format!("写入 HTML 失败:{error}"))); -} - -fn render_state(state: &Value) -> Result { - let trees = state - .get("ui_trees") - .and_then(Value::as_array) - .ok_or("State 缺少 ui_trees")?; - let mut body = String::new(); - for (index, tree) in trees.iter().enumerate() { - if index > 0 { - body.push_str("\n\n"); - } - body.push_str(&render_tree(state, tree)?); - } - Ok(html! { - (PreEscaped("")) - html { - head { - meta charset="utf-8"; - meta name="viewport" content="width=device-width, initial-scale=1"; - style { (PreEscaped("html,body{margin:0;width:100%;min-height:100%;}body{overflow:auto;}[data-ui-tree]{position:relative;width:100%;min-height:0;overflow:hidden;}:root{--ui-scale:1;}")) } - } - body { (PreEscaped(body)) } - } - }.into_string()) -} - -fn render_tree(state: &Value, tree: &Value) -> Result { - let source = tree - .get("src_ui_design") - .and_then(Value::as_str) - .ok_or("UITree 缺少 src_ui_design")?; - let image = state - .get("ui_design_images") - .and_then(|images| images.get(source)) - .ok_or_else(|| format!("缺少设计图资源:{source}"))?; - let width = logical_dimension(image, 0)?; - let height = logical_dimension(image, 1)?; - let root = tree.get("root").ok_or("UITree 缺少 root")?; - let mut content = String::new(); - let tree_metadata = - serde_json::json!({"srcUiDesign": source, "width": width, "height": height}); - write!( - &mut content, - "", - comment_safe(&tree_metadata.to_string()) - ) - .unwrap(); - write!(&mut content, "
", html_escape(source), width, height, width, height).unwrap(); - content.push_str(&render_node(state, root, false)?); - content.push_str("
"); - Ok(content) -} - -fn render_node(state: &Value, node: &Value, in_container: bool) -> Result { - let id = node - .get("id") - .and_then(Value::as_str) - .ok_or("Node 缺少 id")?; - let metadata = node.get("metadata").ok_or("Node 缺少 metadata")?; - let name = metadata - .get("name") - .and_then(Value::as_str) - .unwrap_or_default(); - let description = metadata - .get("description") - .and_then(Value::as_str) - .unwrap_or_default(); - let layout = node.get("layout").ok_or("Node 缺少 layout")?; - let node_metadata = serde_json::json!({"nodeId": id, "name": name, "description": description}); - let mut output = format!( - "", - comment_safe(&node_metadata.to_string()) - ); - if node.get("children_display_mode").and_then(Value::as_str) == Some("Exclusive") { - output.push_str(""); - } - let style = node_style(layout, in_container)?; - let style = format!("{style}border:0;outline:0;background:transparent;overflow:visible;"); - write!( - &mut output, - "
", - html_escape(id), - style - ) - .unwrap(); - if let Some(components) = node.get("components").and_then(Value::as_array) { - for component in components { - output.push_str(&render_component(state, component)?); - } - } - let child_container = layout.get("container").is_some_and(|value| value != "None"); - if let Some(children) = node.get("children").and_then(Value::as_array) { - for child in children { - output.push_str(&render_node(state, child, child_container)?); - } - } - output.push_str("
"); - Ok(output) -} - -fn render_component(state: &Value, component: &Value) -> Result { - if let Some(text) = component.get("Text") { - let content = text - .get("content") - .and_then(Value::as_str) - .unwrap_or_default(); - let size = text - .get("font_sizing") - .and_then(|sizing| { - sizing - .get("Fixed") - .or_else(|| sizing.get("BestFit").and_then(|range| range.get("min"))) - }) - .and_then(Value::as_u64) - .unwrap_or(14); - let alignment = text - .get("alignment") - .and_then(Value::as_str) - .unwrap_or("UpperLeft"); - let horizontal = if alignment.ends_with("Center") { - "center" - } else if alignment.ends_with("Right") { - "right" - } else { - "left" - }; - let vertical = if alignment.starts_with("Middle") { - "center" - } else if alignment.starts_with("Lower") { - "flex-end" - } else { - "flex-start" - }; - let color = text - .get("color") - .and_then(Value::as_array) - .map(|v| { - [ - v.first().and_then(Value::as_u64).unwrap_or(255), - v.get(1).and_then(Value::as_u64).unwrap_or(255), - v.get(2).and_then(Value::as_u64).unwrap_or(255), - v.get(3).and_then(Value::as_u64).unwrap_or(255), - ] - }) - .unwrap_or([255, 255, 255, 255]); - let white_space = - if text.get("horizontal_overflow").and_then(Value::as_str) == Some("Overflow") { - "nowrap" - } else { - "normal" - }; - let overflow = if text.get("vertical_overflow").and_then(Value::as_str) == Some("Overflow") - { - "visible" - } else { - "hidden" - }; - let overflow_wrap = if white_space == "normal" { - "anywhere" - } else { - "normal" - }; - let line_spacing = text - .get("line_spacing") - .and_then(Value::as_f64) - .unwrap_or(1.0); - let (font_weight, font_style) = match text.get("font_style").and_then(Value::as_str) { - Some("Bold") => (700, "normal"), - Some("Italic") => (400, "italic"), - Some("BoldItalic") => (700, "italic"), - _ => (400, "normal"), - }; - let style = format!("position:absolute;inset:0;display:flex;width:100%;height:100%;box-sizing:border-box;color:rgba({},{},{},{});font-size:calc({size}px * var(--ui-scale, 1));font-weight:{font_weight};font-style:{font_style};align-items:{vertical};justify-content:{};text-align:{horizontal};line-height:{line_spacing};white-space:{white_space};overflow:{overflow};overflow-wrap:{overflow_wrap};", color[0], color[1], color[2], color[3] as f32 / 255.0, if horizontal == "left" { "flex-start" } else if horizontal == "center" { "center" } else { "flex-end" }); - return Ok( - html! { div data-component-kind="Text" style=(style) { (content) } }.into_string(), - ); - } - if let Some(image) = component.get("Image") { - let target = image - .get("target_graphic") - .and_then(Value::as_str) - .ok_or("Image 缺少 target_graphic")?; - let sprite = state - .get("sprite_assets") - .and_then(|sprites| sprites.get(target)) - .ok_or_else(|| format!("缺少 Sprite 资源:{target}"))?; - let path = sprite - .get("path") - .and_then(Value::as_str) - .ok_or("Sprite 缺少 path")?; - if path.trim().is_empty() - || path.starts_with('/') - || path.split('/').any(|part| part == "..") - || path.chars().any(|character| { - character.is_control() || matches!(character, '\'' | '"' | '`' | '(' | ')') - }) - { - return Err(format!("资源路径无效:{path}")); - } - let src = format!("/{path}"); - let image_type = image.get("image_type").unwrap_or(&Value::Null); - if let Some(tiled) = image_type.get("Tiled") { - let multiplier = tiled - .get("pixels_per_unit_multiplier") - .and_then(Value::as_f64) - .unwrap_or(1.0); - let w = logical_sprite_dimension(sprite, 0, multiplier)?; - let h = logical_sprite_dimension(sprite, 1, multiplier)?; - let style = format!("position:absolute;inset:0;overflow:hidden;background-image:url(\"{src}\");background-repeat:repeat;background-position:top left;background-size:{w}px {h}px;"); - return Ok(html! { div data-component-kind="Image" style=(style) {} }.into_string()); - } - let mut image_style = "width:100%;height:100%;display:block;object-fit:fill;".to_string(); - if let Some(simple) = image_type.get("Simple") { - if simple - .get("preserve_aspect") - .and_then(Value::as_bool) - .unwrap_or(false) - { - image_style = image_style.replace("fill", "contain"); - } - } - if let Some(filled) = image_type.get("Filled") { - let amount = filled - .get("amount") - .and_then(Value::as_f64) - .unwrap_or(1.0) - .clamp(0.0, 1.0); - if amount < 1.0 { - image_style.push_str(&format!( - "clip-path:inset(0 {}% 0 0);", - (1.0 - amount) * 100.0 - )); - } - } - return Ok(html! { div data-component-kind="Image" style="position:absolute;inset:0;overflow:hidden;" { img src=(src) alt="" aria-hidden="true" draggable="false" style=(image_style); } }.into_string()); - } - Err("未知 UI 组件类型".to_string()) -} - -fn node_style(layout: &Value, in_container: bool) -> Result { - let transform = layout - .get("transform") - .ok_or("ControlLayout 缺少 transform")?; - let amin = pair(transform, "anchor_min")?; - let amax = pair(transform, "anchor_max")?; - let omin = pair(transform, "offset_min")?; - let omax = pair(transform, "offset_max")?; - if amin[0] > amax[0] || amin[1] > amax[1] { - return Err("Transform anchors must be ordered".to_string()); - } - let mut style = if in_container { - "position:relative;".to_string() - } else { - format!( - "position:absolute;left:{};top:{};right:{};bottom:{};", - css_length(amin[0] * 100.0, omin[0])?, - css_length(amin[1] * 100.0, omin[1])?, - css_length((1.0 - amax[0]) * 100.0, -omax[0])?, - css_length((1.0 - amax[1]) * 100.0, -omax[1])? - ) - }; - if let Some(container) = layout.get("container") { - style.push_str(&container_css(container)); - } - Ok(style) -} - -fn container_css(container: &Value) -> String { - if container == "None" { - return String::new(); - } - if let Some(props) = container.get("HBox") { - return format!( - "display:flex;flex-direction:row;gap:calc({}px * var(--ui-scale, 1));", - number(props.get("separation")) - ); - } - if let Some(props) = container.get("VBox") { - return format!( - "display:flex;flex-direction:column;gap:calc({}px * var(--ui-scale, 1));", - number(props.get("separation")) - ); - } - if let Some(props) = container.get("Grid") { - return format!("display:grid;grid-template-columns:repeat({},minmax(0,1fr));column-gap:calc({}px * var(--ui-scale, 1));row-gap:calc({}px * var(--ui-scale, 1));", props.get("columns").and_then(Value::as_u64).unwrap_or(1), number(props.get("h_separation")), number(props.get("v_separation"))); - } - if let Some(props) = container.get("Margin") { - return format!( - "display:grid;padding:calc({}px * var(--ui-scale, 1)) calc({}px * var(--ui-scale, 1)) calc({}px * var(--ui-scale, 1)) calc({}px * var(--ui-scale, 1));", - number(props.get("margin_top")), - number(props.get("margin_right")), - number(props.get("margin_bottom")), - number(props.get("margin_left")) - ); - } - "display:grid;place-items:center;".to_string() -} - -fn css_length(percent: f64, offset: f64) -> Result { - if !percent.is_finite() || !offset.is_finite() { - return Err("Transform 包含非有限数值".to_string()); - } - if percent == 0.0 { - return Ok(format!("calc({offset}px * var(--ui-scale, 1))")); - } - if offset == 0.0 { - return Ok(format!("{percent}%")); - } - Ok(format!( - "calc({percent}% {} {}px * var(--ui-scale, 1))", - if offset < 0.0 { '-' } else { '+' }, - offset.abs() - )) -} -fn pair(value: &Value, field: &str) -> Result<[f64; 2], String> { - let values = value - .get(field) - .and_then(Value::as_array) - .ok_or_else(|| format!("Transform 缺少 {field}"))?; - if values.len() != 2 { - return Err(format!("Transform.{field} 不是二元数组")); - } - Ok([ - values[0].as_f64().ok_or("Transform 数值无效")?, - values[1].as_f64().ok_or("Transform 数值无效")?, - ]) -} -fn logical_dimension(image: &Value, index: usize) -> Result { - let pixels = image - .get("pixel_size") - .and_then(Value::as_array) - .and_then(|v| v.get(index)) - .and_then(Value::as_f64) - .ok_or("设计图 pixel_size 无效")?; - let ppu = image - .get("pixels_per_unit") - .and_then(Value::as_f64) - .ok_or("设计图 pixels_per_unit 无效")?; - if pixels <= 0.0 || ppu <= 0.0 { - return Err("设计图尺寸无效".to_string()); - } - Ok(pixels / ppu) -} -fn logical_sprite_dimension(sprite: &Value, index: usize, multiplier: f64) -> Result { - let pixels = sprite - .get("pixel_size") - .and_then(Value::as_array) - .and_then(|v| v.get(index)) - .and_then(Value::as_f64) - .ok_or("Sprite pixel_size 无效")?; - let ppu = sprite - .get("pixels_per_unit") - .and_then(Value::as_f64) - .ok_or("Sprite pixels_per_unit 无效")?; - if pixels <= 0.0 || ppu <= 0.0 || multiplier <= 0.0 { - return Err("Sprite 尺寸无效".to_string()); - } - Ok(pixels / (ppu * multiplier)) -} -fn number(value: Option<&Value>) -> String { - value - .and_then(Value::as_f64) - .map(|v| v.to_string()) - .unwrap_or_else(|| "0".to_string()) -} -fn comment_safe(value: &str) -> String { - value.replace("--", "- -") -} -fn html_escape(value: &str) -> String { - value - .replace('&', "&") - .replace('"', """) - .replace('<', "<") - .replace('>', ">") -} -fn fail(message: &str) -> ! { - eprintln!("{message}"); - std::process::exit(1) -}