设计UI布局编辑器数据结构
参考godot和ugui的api拆分布局, 资源, 样式 实现ts type binding
This commit is contained in:
+174
@@ -75,6 +75,15 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
@@ -696,6 +705,12 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const_fn"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413d67b29ef1021b4d60f4aa1e925ca031751e213832b4b1d588fae623c05c60"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
@@ -1639,6 +1654,7 @@ dependencies = [
|
||||
"image",
|
||||
"jsonschema",
|
||||
"libc",
|
||||
"nalgebra",
|
||||
"oxc_allocator",
|
||||
"oxc_ast",
|
||||
"oxc_ast_visit",
|
||||
@@ -1664,6 +1680,8 @@ dependencies = [
|
||||
"tauri-plugin-opener",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"ts-rs",
|
||||
"typed_floats",
|
||||
"unicode-normalization",
|
||||
"url",
|
||||
"uuid",
|
||||
@@ -1764,6 +1782,30 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.30.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9"
|
||||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "556f6b2ea90b8d15a74e0e7bb41671c9bdf38cd9f78c284d750b9ce58a2b5be7"
|
||||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.32.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0"
|
||||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.33.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360bd2cd76e0cd9032d42cf2922155cecea2685b0cfa4630c3246df030bcfd6"
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.18.5"
|
||||
@@ -2605,6 +2647,16 @@ dependencies = [
|
||||
"web_atoms",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matrixmultiply"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"rawpointer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
@@ -2694,6 +2746,37 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nalgebra"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adc43a60c217b0c6ff46e47f26911015ad8d2e5a8be1af668c67e370d99a4346"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"glam 0.30.10",
|
||||
"glam 0.31.1",
|
||||
"glam 0.32.1",
|
||||
"glam 0.33.3",
|
||||
"matrixmultiply",
|
||||
"nalgebra-macros",
|
||||
"num-complex",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
"simba",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nalgebra-macros"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "973e7178a678cfd059ccec50887658d482ce16b0aa9da3888ddeab5cd5eb4889"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
@@ -4004,6 +4087,12 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "rawpointer"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@@ -4327,6 +4416,15 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "safe_arch"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a52ec151f024d703f9fd65abb7cbe81e7cdb39f18917a3a37e3014470dc7c59"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -4723,6 +4821,18 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simba"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1a7200d82ff1c7b4235efd12f11e2537a402c91cf83a5cb97ce80eef787fde7"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"wide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.9"
|
||||
@@ -5390,6 +5500,15 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
@@ -5816,6 +5935,28 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs"
|
||||
version = "12.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
|
||||
dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"ts-rs-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs-macros"
|
||||
version = "12.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.28.0"
|
||||
@@ -5833,6 +5974,29 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed_floats"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "436de2fdc42c0f07890f4b51dcc90a468b0b12280f49caff1cf57ce2e646817c"
|
||||
dependencies = [
|
||||
"const_fn",
|
||||
"rustversion",
|
||||
"typed_floats_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed_floats_macros"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da27e33675dd7bfc62c7ead8c36779f961815ababe717191d67b75c2356edc4f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
@@ -6384,6 +6548,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wide"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de2aaf408e58689c2096682331b1f42bb2d9f2ed6b11560407d023cd0a6c634e"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"safe_arch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
||||
@@ -15,6 +15,9 @@ shared-contracts = { path = "../../../server-rs/crates/shared-contracts", defaul
|
||||
tauri-build = { version = "2.6.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
ts-rs = "12.0.1"
|
||||
typed_floats = "1.0.7"
|
||||
nalgebra = "0.35.0"
|
||||
agent-runtime-core = { path = "../../../server-rs/crates/agent-runtime-core" }
|
||||
base64 = "0.22"
|
||||
chromiumoxide = "0.9.1"
|
||||
|
||||
@@ -2482,3 +2482,4 @@ mod diagnostic_log_tests {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod ui_editor;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
use std::{fs, io, path::PathBuf};
|
||||
|
||||
use ts_rs::{Config, TS};
|
||||
|
||||
use super::{
|
||||
component::{
|
||||
image::{
|
||||
FillMethod, HorizontalFillOrigin, ImageComponent, ImageType, Radial180Origin,
|
||||
Radial360Origin, Radial90Origin, VerticalFillOrigin,
|
||||
},
|
||||
text::{
|
||||
BestFitFontSize, FontSizing, FontStyle, HorizontalTextOverflow, TextAlignment,
|
||||
TextComponent, VerticalTextOverflow,
|
||||
},
|
||||
Component,
|
||||
},
|
||||
layout::{dimension::UIRect, node::Node, transform::Transform},
|
||||
resource::{
|
||||
asset_id::{FontAssetId, SpriteAssetId},
|
||||
sprite::{SpriteAsset, SpriteBorder},
|
||||
},
|
||||
};
|
||||
|
||||
const GENERATED_HEADER: &str =
|
||||
"// This file was generated by ts-rs. Do not edit this file manually.";
|
||||
|
||||
fn typescript_bindings() -> String {
|
||||
let config = Config::default();
|
||||
let declarations = [
|
||||
FontAssetId::decl(&config),
|
||||
SpriteAssetId::decl(&config),
|
||||
SpriteBorder::decl(&config),
|
||||
SpriteAsset::decl(&config),
|
||||
UIRect::decl(&config),
|
||||
Transform::decl(&config),
|
||||
HorizontalFillOrigin::decl(&config),
|
||||
VerticalFillOrigin::decl(&config),
|
||||
Radial90Origin::decl(&config),
|
||||
Radial180Origin::decl(&config),
|
||||
Radial360Origin::decl(&config),
|
||||
FillMethod::decl(&config),
|
||||
ImageType::decl(&config),
|
||||
ImageComponent::decl(&config),
|
||||
FontStyle::decl(&config),
|
||||
TextAlignment::decl(&config),
|
||||
HorizontalTextOverflow::decl(&config),
|
||||
VerticalTextOverflow::decl(&config),
|
||||
BestFitFontSize::decl(&config),
|
||||
FontSizing::decl(&config),
|
||||
TextComponent::decl(&config),
|
||||
Component::decl(&config),
|
||||
Node::decl(&config),
|
||||
];
|
||||
|
||||
format!(
|
||||
"{GENERATED_HEADER}\n\nexport {}\n",
|
||||
declarations.join("\n\nexport ")
|
||||
)
|
||||
}
|
||||
|
||||
fn bindings_path() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../src/features/ui-editor/types.ts")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_typescript_bindings() -> io::Result<()> {
|
||||
fs::write(bindings_path(), typescript_bindings())
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ComponentValueError {
|
||||
InvalidAssetId,
|
||||
NonFinite,
|
||||
MustBePositive,
|
||||
MustBeNonNegative,
|
||||
InvalidRange,
|
||||
SpriteBorderExceedsSprite,
|
||||
}
|
||||
|
||||
impl fmt::Display for ComponentValueError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let message = match self {
|
||||
Self::InvalidAssetId => "资源 ID 不能为空或包含首尾空白",
|
||||
Self::NonFinite => "数值必须是有限数",
|
||||
Self::MustBePositive => "数值必须大于零",
|
||||
Self::MustBeNonNegative => "数值不能小于零",
|
||||
Self::InvalidRange => "数值范围无效",
|
||||
Self::SpriteBorderExceedsSprite => "九宫格边距不能超过 Sprite 尺寸",
|
||||
};
|
||||
formatter.write_str(message)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ComponentValueError {}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod error;
|
||||
@@ -0,0 +1,213 @@
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
use crate::ui_editor::component::common::error::ComponentValueError;
|
||||
use crate::ui_editor::layout::transform::Transform;
|
||||
use crate::ui_editor::resource::sprite::SpriteAsset;
|
||||
use nalgebra::Vector2;
|
||||
use ts_rs::TS;
|
||||
use typed_floats::tf32::StrictlyPositiveFinite;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, TS)]
|
||||
pub enum HorizontalFillOrigin {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, TS)]
|
||||
pub enum VerticalFillOrigin {
|
||||
Bottom,
|
||||
Top,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, TS)]
|
||||
pub enum Radial90Origin {
|
||||
BottomLeft,
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, TS)]
|
||||
pub enum Radial180Origin {
|
||||
Bottom,
|
||||
Left,
|
||||
Top,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, TS)]
|
||||
pub enum Radial360Origin {
|
||||
Bottom,
|
||||
Right,
|
||||
Top,
|
||||
Left,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, TS)]
|
||||
pub enum FillMethod {
|
||||
Horizontal(HorizontalFillOrigin),
|
||||
Vertical(VerticalFillOrigin),
|
||||
Radial90 {
|
||||
origin: Radial90Origin,
|
||||
clockwise: bool,
|
||||
},
|
||||
Radial180 {
|
||||
origin: Radial180Origin,
|
||||
clockwise: bool,
|
||||
},
|
||||
Radial360 {
|
||||
origin: Radial360Origin,
|
||||
clockwise: bool,
|
||||
},
|
||||
}
|
||||
|
||||
fn validate_fill_amount(amount: f32) -> Result<(), ComponentValueError> {
|
||||
if !amount.is_finite() {
|
||||
return Err(ComponentValueError::NonFinite);
|
||||
}
|
||||
if !(0.0..=1.0).contains(&amount) {
|
||||
return Err(ComponentValueError::InvalidRange);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, TS)]
|
||||
pub enum ImageType {
|
||||
Simple {
|
||||
preserve_aspect: bool,
|
||||
},
|
||||
Sliced {
|
||||
fill_center: bool,
|
||||
#[ts(as = "f32")]
|
||||
pixels_per_unit_multiplier: StrictlyPositiveFinite,
|
||||
},
|
||||
Tiled {
|
||||
fill_center: bool,
|
||||
#[ts(as = "f32")]
|
||||
pixels_per_unit_multiplier: StrictlyPositiveFinite,
|
||||
},
|
||||
Filled {
|
||||
preserve_aspect: bool,
|
||||
method: FillMethod,
|
||||
amount: f32,
|
||||
},
|
||||
}
|
||||
|
||||
impl ImageType {
|
||||
pub const fn sliced(
|
||||
fill_center: bool,
|
||||
pixels_per_unit_multiplier: StrictlyPositiveFinite,
|
||||
) -> Self {
|
||||
Self::Sliced {
|
||||
fill_center,
|
||||
pixels_per_unit_multiplier,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn tiled(
|
||||
fill_center: bool,
|
||||
pixels_per_unit_multiplier: StrictlyPositiveFinite,
|
||||
) -> Self {
|
||||
Self::Tiled {
|
||||
fill_center,
|
||||
pixels_per_unit_multiplier,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filled(
|
||||
preserve_aspect: bool,
|
||||
method: FillMethod,
|
||||
amount: f32,
|
||||
) -> Result<Self, ComponentValueError> {
|
||||
validate_fill_amount(amount)?;
|
||||
Ok(Self::Filled {
|
||||
preserve_aspect,
|
||||
method,
|
||||
amount,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn supports_native_size(self) -> bool {
|
||||
matches!(self, Self::Simple { .. } | Self::Filled { .. })
|
||||
}
|
||||
|
||||
pub fn uses_sprite_border(self) -> bool {
|
||||
matches!(self, Self::Sliced { .. } | Self::Tiled { .. })
|
||||
}
|
||||
|
||||
pub fn tiles_center(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Tiled {
|
||||
fill_center: true,
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ImageType {
|
||||
fn default() -> Self {
|
||||
Self::Simple {
|
||||
preserve_aspect: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum SetNativeSizeError {
|
||||
MissingSprite,
|
||||
UnsupportedImageType,
|
||||
}
|
||||
|
||||
impl fmt::Display for SetNativeSizeError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let message = match self {
|
||||
Self::MissingSprite => "Image 没有 Target Graphic Sprite",
|
||||
Self::UnsupportedImageType => "只有 Simple 或 Filled Image 支持 Set Native Size",
|
||||
};
|
||||
formatter.write_str(message)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for SetNativeSizeError {}
|
||||
|
||||
/// Image 渲染组件;布局由同一 Entity 上独立的 `Control` 提供。
|
||||
#[derive(Clone, Debug, PartialEq, TS)]
|
||||
pub struct ImageComponent {
|
||||
pub target_graphic: Option<SpriteAsset>,
|
||||
pub image_type: ImageType,
|
||||
}
|
||||
|
||||
impl ImageComponent {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
target_graphic: None,
|
||||
image_type: ImageType::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 跨组件把布局重置为 Sprite 的原始尺寸。
|
||||
pub fn set_native_size(
|
||||
&self,
|
||||
control: &mut Transform,
|
||||
) -> Result<Vector2<f32>, SetNativeSizeError> {
|
||||
if !self.image_type.supports_native_size() {
|
||||
return Err(SetNativeSizeError::UnsupportedImageType);
|
||||
}
|
||||
let native_size = self
|
||||
.target_graphic
|
||||
.as_ref()
|
||||
.ok_or(SetNativeSizeError::MissingSprite)?
|
||||
.native_size();
|
||||
control.anchor_max = control.anchor_min;
|
||||
control.offset_max = control.offset_min + native_size;
|
||||
Ok(native_size)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ImageComponent {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod common;
|
||||
pub mod image;
|
||||
pub mod text;
|
||||
#[derive(ts_rs::TS)]
|
||||
pub enum Component {
|
||||
Image(image::ImageComponent),
|
||||
Text(text::TextComponent),
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
use crate::ui_editor::component::common::error::ComponentValueError;
|
||||
use crate::ui_editor::resource::asset_id::FontAssetId;
|
||||
use crate::ui_editor::resource::sprite::{Color, WHITE};
|
||||
use std::num::NonZeroU32;
|
||||
use ts_rs::TS;
|
||||
use typed_floats::tf32::StrictlyPositiveFinite;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, TS)]
|
||||
pub enum FontStyle {
|
||||
Normal,
|
||||
Bold,
|
||||
Italic,
|
||||
BoldItalic,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, TS)]
|
||||
pub enum TextAlignment {
|
||||
UpperLeft,
|
||||
UpperCenter,
|
||||
UpperRight,
|
||||
MiddleLeft,
|
||||
MiddleCenter,
|
||||
MiddleRight,
|
||||
LowerLeft,
|
||||
LowerCenter,
|
||||
LowerRight,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, TS)]
|
||||
pub enum HorizontalTextOverflow {
|
||||
Wrap,
|
||||
Overflow,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, TS)]
|
||||
pub enum VerticalTextOverflow {
|
||||
Truncate,
|
||||
Overflow,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, TS)]
|
||||
pub struct BestFitFontSize {
|
||||
min: NonZeroU32,
|
||||
max: NonZeroU32,
|
||||
}
|
||||
|
||||
impl BestFitFontSize {
|
||||
pub fn new(min: NonZeroU32, max: NonZeroU32) -> Result<Self, ComponentValueError> {
|
||||
if min > max {
|
||||
return Err(ComponentValueError::InvalidRange);
|
||||
}
|
||||
Ok(Self { min, max })
|
||||
}
|
||||
|
||||
pub fn min(self) -> NonZeroU32 {
|
||||
self.min
|
||||
}
|
||||
|
||||
pub fn max(self) -> NonZeroU32 {
|
||||
self.max
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, TS)]
|
||||
pub enum FontSizing {
|
||||
Fixed(NonZeroU32),
|
||||
BestFit(BestFitFontSize),
|
||||
}
|
||||
|
||||
impl Default for FontSizing {
|
||||
fn default() -> Self {
|
||||
Self::Fixed(NonZeroU32::new(14).expect("14 is non-zero"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Text / Label 渲染组件;布局由同一 Entity 上独立的 `Control` 提供。
|
||||
#[derive(Clone, Debug, PartialEq, TS)]
|
||||
pub struct TextComponent {
|
||||
pub content: String,
|
||||
pub font: Option<FontAssetId>,
|
||||
pub font_style: FontStyle,
|
||||
pub font_sizing: FontSizing,
|
||||
#[ts(as = "[u8; 4]")]
|
||||
pub color: Color,
|
||||
pub alignment: TextAlignment,
|
||||
pub horizontal_overflow: HorizontalTextOverflow,
|
||||
pub vertical_overflow: VerticalTextOverflow,
|
||||
#[ts(as = "f32")]
|
||||
pub line_spacing: StrictlyPositiveFinite,
|
||||
}
|
||||
|
||||
impl TextComponent {
|
||||
pub fn new(content: impl Into<String>) -> Self {
|
||||
Self {
|
||||
content: content.into(),
|
||||
font: None,
|
||||
font_style: FontStyle::Normal,
|
||||
font_sizing: FontSizing::default(),
|
||||
color: WHITE,
|
||||
alignment: TextAlignment::UpperLeft,
|
||||
horizontal_overflow: HorizontalTextOverflow::Wrap,
|
||||
vertical_overflow: VerticalTextOverflow::Truncate,
|
||||
line_spacing: StrictlyPositiveFinite::new(1.0).expect("1.0 is positive and finite"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TextComponent {
|
||||
fn default() -> Self {
|
||||
Self::new(String::new())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod ui_design_image;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
pub enum UIDesignImageRole{
|
||||
Page,
|
||||
Section,
|
||||
Modal,
|
||||
Drawer,
|
||||
Popover,
|
||||
State,
|
||||
Scrolled,
|
||||
Detail,
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use nalgebra::{Point2, Vector2};
|
||||
use ts_rs::TS;
|
||||
|
||||
/// UI 布局在父级坐标系中的轴对齐矩形。
|
||||
///
|
||||
/// `min` 是矩形的最小角,`size` 是沿两个坐标轴的尺寸。这里不规定 Y 轴方向,
|
||||
/// 因而既能用于 Y 轴向上的游戏坐标,也能用于 Y 轴向下的画布坐标。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, TS)]
|
||||
pub struct UIRect {
|
||||
#[ts(as = "[f32; 2]")]
|
||||
pub min: Point2<f32>,
|
||||
#[ts(as = "[f32; 2]")]
|
||||
pub size: Vector2<f32>,
|
||||
}
|
||||
|
||||
impl UIRect {
|
||||
pub fn new(min: Point2<f32>, size: Vector2<f32>) -> Self {
|
||||
Self { min, size }
|
||||
}
|
||||
|
||||
pub fn from_min_max(min: Point2<f32>, max: Point2<f32>) -> Self {
|
||||
Self::new(min, max - min)
|
||||
}
|
||||
|
||||
pub fn max(&self) -> Point2<f32> {
|
||||
self.min + self.size
|
||||
}
|
||||
|
||||
pub fn center(&self) -> Point2<f32> {
|
||||
self.min + self.size * 0.5
|
||||
}
|
||||
|
||||
/// 把矩形内部的归一化坐标转换为当前坐标系中的点。
|
||||
pub fn point_at(&self, normalized_position: Vector2<f32>) -> Point2<f32> {
|
||||
self.min + self.size.component_mul(&normalized_position)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod dimension;
|
||||
pub mod node;
|
||||
pub mod transform;
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::ui_editor::component::Component;
|
||||
use crate::ui_editor::layout::transform::Transform;
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(TS)]
|
||||
pub struct Node {
|
||||
transform: Transform,
|
||||
components: Vec<Component>,
|
||||
children: Vec<Node>,
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
use crate::ui_editor::layout::dimension::UIRect;
|
||||
use nalgebra::Vector2;
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, TS)]
|
||||
pub struct Transform {
|
||||
#[ts(as = "[f32; 2]")]
|
||||
pub anchor_min: Vector2<f32>,
|
||||
#[ts(as = "[f32; 2]")]
|
||||
pub anchor_max: Vector2<f32>,
|
||||
#[ts(as = "[f32; 2]")]
|
||||
pub offset_min: Vector2<f32>,
|
||||
#[ts(as = "[f32; 2]")]
|
||||
pub offset_max: Vector2<f32>,
|
||||
}
|
||||
|
||||
impl Transform {
|
||||
pub fn new(
|
||||
anchor_min: Vector2<f32>,
|
||||
anchor_max: Vector2<f32>,
|
||||
offset_min: Vector2<f32>,
|
||||
offset_max: Vector2<f32>,
|
||||
) -> Self {
|
||||
Self {
|
||||
anchor_min,
|
||||
anchor_max,
|
||||
offset_min,
|
||||
offset_max,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建中心锚定、指定尺寸的矩形。
|
||||
pub fn centered(size: Vector2<f32>) -> Self {
|
||||
let half_size = size * 0.5;
|
||||
Self::new(
|
||||
Vector2::new(0.5, 0.5),
|
||||
Vector2::new(0.5, 0.5),
|
||||
-half_size,
|
||||
half_size,
|
||||
)
|
||||
}
|
||||
|
||||
/// 创建铺满父矩形的拉伸布局。
|
||||
pub fn stretch() -> Self {
|
||||
Self::new(
|
||||
Vector2::zeros(),
|
||||
Vector2::repeat(1.0),
|
||||
Vector2::zeros(),
|
||||
Vector2::zeros(),
|
||||
)
|
||||
}
|
||||
|
||||
/// 锚点在父矩形中覆盖的尺寸。
|
||||
pub fn anchor_span_size(&self, parent: &UIRect) -> Vector2<f32> {
|
||||
parent
|
||||
.size
|
||||
.component_mul(&(self.anchor_max - self.anchor_min))
|
||||
}
|
||||
|
||||
/// 在给定父矩形下得到最终尺寸。
|
||||
pub fn resolved_size(&self, parent: &UIRect) -> Vector2<f32> {
|
||||
self.anchor_span_size(parent) + self.offset_max - self.offset_min
|
||||
}
|
||||
|
||||
/// 在给定父矩形下解析出最终轴对齐矩形。
|
||||
pub fn resolve(&self, parent: &UIRect) -> UIRect {
|
||||
let min = parent.point_at(self.anchor_min) + self.offset_min;
|
||||
let max = parent.point_at(self.anchor_max) + self.offset_max;
|
||||
UIRect::from_min_max(min, max)
|
||||
}
|
||||
|
||||
/// 调整 offsets,使解析结果匹配目标矩形,anchors 保持不变。
|
||||
pub fn set_resolved_rect(&mut self, parent: &UIRect, rect: UIRect) {
|
||||
self.offset_min = rect.min - parent.point_at(self.anchor_min);
|
||||
self.offset_max = rect.max() - parent.point_at(self.anchor_max);
|
||||
}
|
||||
|
||||
/// 修改 anchors,同时保持当前可见矩形不变。
|
||||
pub fn set_anchors_preserving_rect(
|
||||
&mut self,
|
||||
parent: &UIRect,
|
||||
anchor_min: Vector2<f32>,
|
||||
anchor_max: Vector2<f32>,
|
||||
) {
|
||||
let rect = self.resolve(parent);
|
||||
self.anchor_min = anchor_min;
|
||||
self.anchor_max = anchor_max;
|
||||
self.set_resolved_rect(parent, rect);
|
||||
}
|
||||
|
||||
/// 保持最小边不变,直接设置最终尺寸。
|
||||
pub fn set_size(&mut self, size: Vector2<f32>, parent: &UIRect) {
|
||||
self.offset_max = self.offset_min + size - self.anchor_span_size(parent);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Transform {
|
||||
fn default() -> Self {
|
||||
Self::centered(Vector2::new(100.0, 100.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ui_editor::layout::dimension::UIRect;
|
||||
use crate::ui_editor::layout::transform::Transform;
|
||||
use nalgebra::{Point2, Vector2};
|
||||
|
||||
fn assert_vector_close(actual: Vector2<f32>, expected: Vector2<f32>) {
|
||||
assert!(
|
||||
(actual - expected).norm() <= 1.0e-4,
|
||||
"actual={actual:?}, expected={expected:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_point_close(actual: Point2<f32>, expected: Point2<f32>) {
|
||||
assert_vector_close(actual - expected, Vector2::zeros());
|
||||
}
|
||||
|
||||
fn assert_rect_close(actual: UIRect, expected: UIRect) {
|
||||
assert_point_close(actual.min, expected.min);
|
||||
assert_vector_close(actual.size, expected.size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_anchors_use_offsets_as_final_rect() {
|
||||
let parent = UIRect::new(Point2::new(10.0, 20.0), Vector2::new(800.0, 600.0));
|
||||
let control = Transform::new(
|
||||
Vector2::new(0.5, 0.5),
|
||||
Vector2::new(0.5, 0.5),
|
||||
Vector2::new(-20.0, -30.0),
|
||||
Vector2::new(80.0, 10.0),
|
||||
);
|
||||
|
||||
assert_rect_close(
|
||||
control.resolve(&parent),
|
||||
UIRect::new(Point2::new(390.0, 290.0), Vector2::new(100.0, 40.0)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_anchors_add_parent_span_to_offsets() {
|
||||
let parent = UIRect::new(Point2::new(10.0, 20.0), Vector2::new(800.0, 600.0));
|
||||
let control = Transform::new(
|
||||
Vector2::new(0.1, 0.2),
|
||||
Vector2::new(0.9, 0.8),
|
||||
Vector2::new(10.0, 25.0),
|
||||
Vector2::new(-10.0, -15.0),
|
||||
);
|
||||
|
||||
assert_rect_close(
|
||||
control.resolve(&parent),
|
||||
UIRect::new(Point2::new(100.0, 165.0), Vector2::new(620.0, 320.0)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stretch_fills_a_parent_with_a_non_zero_origin() {
|
||||
let parent = UIRect::new(Point2::new(-20.0, 35.0), Vector2::new(320.0, 180.0));
|
||||
|
||||
assert_rect_close(Transform::stretch().resolve(&parent), parent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_rect_can_be_converted_back_to_layout_properties() {
|
||||
let parent = UIRect::new(Point2::new(100.0, -50.0), Vector2::new(900.0, 500.0));
|
||||
let expected = UIRect::new(Point2::new(240.0, 80.0), Vector2::new(360.0, 140.0));
|
||||
let mut control = Transform::new(
|
||||
Vector2::new(0.2, 0.1),
|
||||
Vector2::new(0.8, 0.9),
|
||||
Vector2::zeros(),
|
||||
Vector2::zeros(),
|
||||
);
|
||||
|
||||
control.set_resolved_rect(&parent, expected);
|
||||
|
||||
assert_rect_close(control.resolve(&parent), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_anchors_can_preserve_the_visible_rect() {
|
||||
let parent = UIRect::new(Point2::new(20.0, 30.0), Vector2::new(1280.0, 720.0));
|
||||
let mut control = Transform::new(
|
||||
Vector2::new(0.0, 0.0),
|
||||
Vector2::new(0.0, 0.0),
|
||||
Vector2::new(120.0, 80.0),
|
||||
Vector2::new(420.0, 240.0),
|
||||
);
|
||||
let expected = control.resolve(&parent);
|
||||
|
||||
control.set_anchors_preserving_rect(
|
||||
&parent,
|
||||
Vector2::new(0.2, 0.3),
|
||||
Vector2::new(0.8, 0.7),
|
||||
);
|
||||
assert_rect_close(control.resolve(&parent), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_size_accounts_for_parent_anchor_span() {
|
||||
let parent = UIRect::new(Point2::origin(), Vector2::new(1000.0, 600.0));
|
||||
let mut control = Transform::new(
|
||||
Vector2::new(0.25, 0.2),
|
||||
Vector2::new(0.75, 0.8),
|
||||
Vector2::zeros(),
|
||||
Vector2::zeros(),
|
||||
);
|
||||
|
||||
control.set_size(Vector2::new(640.0, 240.0), &parent);
|
||||
|
||||
assert_vector_close(control.resolved_size(&parent), Vector2::new(640.0, 240.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod component;
|
||||
pub mod layout;
|
||||
pub mod resource;
|
||||
|
||||
#[cfg(test)]
|
||||
mod bindings;
|
||||
pub mod import;
|
||||
@@ -0,0 +1,31 @@
|
||||
use crate::ui_editor::component::common::error::ComponentValueError;
|
||||
|
||||
macro_rules! define_asset_id {
|
||||
($name:ident) => {
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, ts_rs::TS)]
|
||||
pub struct $name(String);
|
||||
|
||||
impl $name {
|
||||
pub fn new(value: impl Into<String>) -> Result<Self, ComponentValueError> {
|
||||
let value = value.into();
|
||||
if value.is_empty() || value.trim() != value {
|
||||
return Err(ComponentValueError::InvalidAssetId);
|
||||
}
|
||||
Ok(Self(value))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for $name {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
define_asset_id!(FontAssetId);
|
||||
define_asset_id!(SpriteAssetId);
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod asset_id;
|
||||
pub mod sprite;
|
||||
@@ -0,0 +1,125 @@
|
||||
use crate::ui_editor::component::common::error::ComponentValueError;
|
||||
use crate::ui_editor::resource::asset_id::SpriteAssetId;
|
||||
use image::Rgba;
|
||||
use nalgebra::Vector2;
|
||||
use ts_rs::TS;
|
||||
use typed_floats::tf32::StrictlyPositiveFinite;
|
||||
|
||||
pub type Color = Rgba<u8>;
|
||||
|
||||
pub(in crate::ui_editor) const WHITE: Color = Rgba([255, 255, 255, 255]);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, TS)]
|
||||
pub struct SpriteBorder {
|
||||
left: f32,
|
||||
right: f32,
|
||||
top: f32,
|
||||
bottom: f32,
|
||||
}
|
||||
|
||||
impl SpriteBorder {
|
||||
pub const NONE: Self = Self {
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
};
|
||||
|
||||
pub fn new(left: f32, right: f32, top: f32, bottom: f32) -> Result<Self, ComponentValueError> {
|
||||
let values = [left, right, top, bottom];
|
||||
if values.iter().any(|value| !value.is_finite()) {
|
||||
return Err(ComponentValueError::NonFinite);
|
||||
}
|
||||
if values.iter().any(|value| *value < 0.0) {
|
||||
return Err(ComponentValueError::MustBeNonNegative);
|
||||
}
|
||||
Ok(Self {
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn left(self) -> f32 {
|
||||
self.left
|
||||
}
|
||||
|
||||
pub fn right(self) -> f32 {
|
||||
self.right
|
||||
}
|
||||
|
||||
pub fn top(self) -> f32 {
|
||||
self.top
|
||||
}
|
||||
|
||||
pub fn bottom(self) -> f32 {
|
||||
self.bottom
|
||||
}
|
||||
|
||||
pub fn has_border(self) -> bool {
|
||||
self != Self::NONE
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SpriteBorder {
|
||||
fn default() -> Self {
|
||||
Self::NONE
|
||||
}
|
||||
}
|
||||
|
||||
/// UI Sprite 引用以及 `Set Native Size` / 9-slice 所需的导入元数据。
|
||||
#[derive(Clone, Debug, PartialEq, TS)]
|
||||
pub struct SpriteAsset {
|
||||
asset_id: SpriteAssetId,
|
||||
#[ts(as = "[f32; 2]")]
|
||||
pixel_size: Vector2<f32>,
|
||||
#[ts(as = "f32")]
|
||||
pixels_per_unit: StrictlyPositiveFinite,
|
||||
border: SpriteBorder,
|
||||
}
|
||||
|
||||
impl SpriteAsset {
|
||||
pub fn new(
|
||||
asset_id: SpriteAssetId,
|
||||
pixel_size: Vector2<f32>,
|
||||
pixels_per_unit: StrictlyPositiveFinite,
|
||||
border: SpriteBorder,
|
||||
) -> Result<Self, ComponentValueError> {
|
||||
if !pixel_size.x.is_finite() || !pixel_size.y.is_finite() {
|
||||
return Err(ComponentValueError::NonFinite);
|
||||
}
|
||||
if pixel_size.x <= 0.0 || pixel_size.y <= 0.0 {
|
||||
return Err(ComponentValueError::MustBePositive);
|
||||
}
|
||||
if border.left + border.right > pixel_size.x || border.top + border.bottom > pixel_size.y {
|
||||
return Err(ComponentValueError::SpriteBorderExceedsSprite);
|
||||
}
|
||||
Ok(Self {
|
||||
asset_id,
|
||||
pixel_size,
|
||||
pixels_per_unit,
|
||||
border,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn asset_id(&self) -> &SpriteAssetId {
|
||||
&self.asset_id
|
||||
}
|
||||
|
||||
pub fn pixel_size(&self) -> Vector2<f32> {
|
||||
self.pixel_size
|
||||
}
|
||||
|
||||
pub fn pixels_per_unit(&self) -> StrictlyPositiveFinite {
|
||||
self.pixels_per_unit
|
||||
}
|
||||
|
||||
pub fn border(&self) -> SpriteBorder {
|
||||
self.border
|
||||
}
|
||||
|
||||
pub fn native_size(&self) -> Vector2<f32> {
|
||||
self.pixel_size / self.pixels_per_unit.get()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// This file was generated by ts-rs. Do not edit this file manually.
|
||||
|
||||
export type FontAssetId = string;
|
||||
|
||||
export type SpriteAssetId = string;
|
||||
|
||||
export type SpriteBorder = { left: number, right: number, top: number, bottom: number, };
|
||||
|
||||
export type SpriteAsset = { asset_id: SpriteAssetId, pixel_size: [number, number], pixels_per_unit: number, border: SpriteBorder, };
|
||||
|
||||
export type UIRect = { min: [number, number], size: [number, number], };
|
||||
|
||||
export type Transform = { anchor_min: [number, number], anchor_max: [number, number], offset_min: [number, number], offset_max: [number, number], };
|
||||
|
||||
export type HorizontalFillOrigin = "Left" | "Right";
|
||||
|
||||
export type VerticalFillOrigin = "Bottom" | "Top";
|
||||
|
||||
export type Radial90Origin = "BottomLeft" | "TopLeft" | "TopRight" | "BottomRight";
|
||||
|
||||
export type Radial180Origin = "Bottom" | "Left" | "Top" | "Right";
|
||||
|
||||
export type Radial360Origin = "Bottom" | "Right" | "Top" | "Left";
|
||||
|
||||
export type FillMethod = { "Horizontal": HorizontalFillOrigin } | { "Vertical": VerticalFillOrigin } | { "Radial90": { origin: Radial90Origin, clockwise: boolean, } } | { "Radial180": { origin: Radial180Origin, clockwise: boolean, } } | { "Radial360": { origin: Radial360Origin, clockwise: boolean, } };
|
||||
|
||||
export type ImageType = { "Simple": { preserve_aspect: boolean, } } | { "Sliced": { fill_center: boolean, pixels_per_unit_multiplier: number, } } | { "Tiled": { fill_center: boolean, pixels_per_unit_multiplier: number, } } | { "Filled": { preserve_aspect: boolean, method: FillMethod, amount: number, } };
|
||||
|
||||
export type ImageComponent = { target_graphic: SpriteAsset | null, image_type: ImageType, };
|
||||
|
||||
export type FontStyle = "Normal" | "Bold" | "Italic" | "BoldItalic";
|
||||
|
||||
export type TextAlignment = "UpperLeft" | "UpperCenter" | "UpperRight" | "MiddleLeft" | "MiddleCenter" | "MiddleRight" | "LowerLeft" | "LowerCenter" | "LowerRight";
|
||||
|
||||
export type HorizontalTextOverflow = "Wrap" | "Overflow";
|
||||
|
||||
export type VerticalTextOverflow = "Truncate" | "Overflow";
|
||||
|
||||
export type BestFitFontSize = { min: number, max: number, };
|
||||
|
||||
export type FontSizing = { "Fixed": number } | { "BestFit": BestFitFontSize };
|
||||
|
||||
export type TextComponent = { content: string, font: FontAssetId | null, font_style: FontStyle, font_sizing: FontSizing, color: [number, number, number, number], alignment: TextAlignment, horizontal_overflow: HorizontalTextOverflow, vertical_overflow: VerticalTextOverflow, line_spacing: number, };
|
||||
|
||||
export type Component = { "Image": ImageComponent } | { "Text": TextComponent };
|
||||
|
||||
export type Node = { transform: Transform, components: Array<Component>, children: Array<Node>, };
|
||||
Reference in New Issue
Block a user