From 0e069bd4388e21174a1da06015372f26f4ad2493 Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Sun, 10 Aug 2025 14:52:35 +0300 Subject: [PATCH] scale and rotate --- flumi/Scripts/B9/CSSParser.gd | 12 ++ flumi/Scripts/Constants.gd | 137 +++++++++++++++- flumi/Scripts/StyleManager.gd | 93 +++++++++++ flumi/Scripts/Utils/BackgroundUtils.gd | 19 +++ flumi/Scripts/Utils/TransformUtils.gd | 158 +++++++++++++++++++ flumi/Scripts/Utils/TransformUtils.gd.uid | 1 + flumi/Scripts/Utils/UtilityClassValidator.gd | 4 + 7 files changed, 422 insertions(+), 2 deletions(-) create mode 100644 flumi/Scripts/Utils/TransformUtils.gd create mode 100644 flumi/Scripts/Utils/TransformUtils.gd.uid diff --git a/flumi/Scripts/B9/CSSParser.gd b/flumi/Scripts/B9/CSSParser.gd index a5b45a5..c147413 100644 --- a/flumi/Scripts/B9/CSSParser.gd +++ b/flumi/Scripts/B9/CSSParser.gd @@ -897,6 +897,18 @@ static func parse_utility_class_internal(rule: CSSRule, utility_name: String) -> "object-contain": rule.properties["object-fit"] = "contain"; return "object-cover": rule.properties["object-fit"] = "cover"; return + if utility_name.begins_with("scale-") or utility_name.begins_with("scale-x-") or utility_name.begins_with("scale-y-"): + var transform_properties = TransformUtils.parse_scale_utility(utility_name) + for property in transform_properties: + rule.properties[property] = transform_properties[property] + return + + if utility_name.begins_with("rotate-") or utility_name.begins_with("rotate-x-") or utility_name.begins_with("rotate-y-"): + var transform_properties = TransformUtils.parse_rotation_utility(utility_name) + for property in transform_properties: + rule.properties[property] = transform_properties[property] + return + # Handle more utility classes as needed # Add more cases here for other utilities diff --git a/flumi/Scripts/Constants.gd b/flumi/Scripts/Constants.gd index 0f73ec0..3fbf404 100644 --- a/flumi/Scripts/Constants.gd +++ b/flumi/Scripts/Constants.gd @@ -2523,6 +2523,139 @@ var HTML_CONTENT = """ """.to_utf8_buffer() -# Set the active HTML content to use the tween demo +var HTML_CONTENT_TRANSFORM_TEST = """ + Transform CSS Demo - Scale & Rotation + + + + + + + +

🔄 Scale & Rotation CSS Demonstration

+ +
+ +
+

Normal

+
No transform applied
+
Box
+
No transform
+
+ + +
+

Scale 50%

+
Scaled to 50% size
+
Box
+
scale-50
+
+ + +
+

Scale 150%

+
Scaled to 150% size
+
Box
+
scale-150
+
+ + +
+

Scale X 75%

+
X-axis scaled to 75%
+
Box
+
scale-x-75
+
+ + +
+

Scale Y 125%

+
Y-axis scaled to 125%
+
Box
+
scale-y-125
+
+ + +
+

Custom Scale

+
Custom scale value 1.7
+
Box
+
scale-[170]
+
+ + +
+

Rotate 45°

+
Rotated 45 degrees
+
Box
+
rotate-45
+
+ + +
+

Rotate 90°

+
Rotated 90 degrees
+
Box
+
rotate-90
+
+ + +
+

Custom Rotate

+
Custom rotation 30deg
+
Box
+
rotate-[30deg]
+
+ + +
+

Scale + Rotate

+
Scale 125% and rotate 45°
+
Box
+
scale-125 rotate-45
+
+ + +
+

Radians

+
Rotation using radians
+
Box
+
rotate-[1.57rad]
+
+ + +
+

Hover Effect

+
Hover to see scale effect
+
Box
+
hover:scale-110
+
+ +
+ +
+

📝 Transform Utility Reference

+
+

Scale utilities: scale-{value}, scale-x-{value}, scale-y-{value}

+

Named values: 0, 50, 75, 90, 95, 100, 105, 110, 125, 150, 200

+

Custom values: scale-[{number}] where number is percentage (100 = 1.0)

+

Rotation utilities: rotate-{degrees}, rotate-x-{degrees}, rotate-y-{degrees}

+

Named degrees: 0, 1, 2, 3, 6, 12, 45, 90, 180, 270

+

Custom rotations: rotate-[{value}deg], rotate-[{value}rad], rotate-[{value}turn]

+
+
+ +""".to_utf8_buffer() + +# Set the active HTML content to use the transform demo func _ready(): - HTML_CONTENT = HTML_CONTENT_TWEEN + HTML_CONTENT = HTML_CONTENT_TRANSFORM_TEST diff --git a/flumi/Scripts/StyleManager.gd b/flumi/Scripts/StyleManager.gd index bac6d76..d222a55 100644 --- a/flumi/Scripts/StyleManager.gd +++ b/flumi/Scripts/StyleManager.gd @@ -181,6 +181,14 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement, if label: apply_styles_to_label(label, styles, element, parser) + var transform_target = node + + if node is MarginContainer and node.name.begins_with("MarginWrapper_"): + if node.get_child_count() > 0: + transform_target = node.get_child(0) + + apply_transform_properties(transform_target, styles) + return node static func apply_stylebox_to_panel_container(panel_container: PanelContainer, styles: Dictionary) -> void: @@ -651,3 +659,88 @@ static func apply_image_styles(image_node: Control, styles: Dictionary) -> void: texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT "cover": texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED + +static func apply_transform_properties(node: Control, styles: Dictionary) -> void: + if node is FlexContainer: + var has_panel_children = false + for child in node.get_children(): + if child is PanelContainer: + has_panel_children = true + break + + if has_panel_children: + apply_transform_properties_direct(node, styles) + else: + for child in node.get_children(): + if child is RichTextLabel or child is Control: + apply_transform_properties_direct(child, styles) + return + apply_transform_properties_direct(node, styles) + else: + apply_transform_properties_direct(node, styles) + +static func apply_transform_properties_direct(node: Control, styles: Dictionary) -> void: + var has_transform = false + var scale_x = 1.0 + var scale_y = 1.0 + var rotation_z = 0.0 + + # Check for scale properties + if styles.has("scale-x"): + scale_x = styles["scale-x"] + has_transform = true + if styles.has("scale-y"): + scale_y = styles["scale-y"] + has_transform = true + + # Check for rotation properties (prioritize Z-axis for 2D) + if styles.has("rotate-z"): + rotation_z = styles["rotate-z"] + has_transform = true + elif styles.has("rotate-x"): + rotation_z = styles["rotate-x"] + has_transform = true + elif styles.has("rotate-y"): + rotation_z = styles["rotate-y"] + has_transform = true + + if has_transform: + # Set metadata flag to tell FlexContainer to preserve transforms + node.set_meta("css_transform_applied", true) + node.set_meta("pending_scale", Vector2(scale_x, scale_y)) + node.set_meta("pending_rotation", rotation_z) + + # Apply immediately + node.scale = Vector2(scale_x, scale_y) + node.rotation = rotation_z + + # Reapply after FlexContainer layout using timer + var timer = Timer.new() + timer.wait_time = 0.1 + timer.one_shot = true + timer.autostart = true + var main_scene = Engine.get_main_loop().current_scene + if main_scene: + timer.timeout.connect(func(): _reapply_transforms(node)) + main_scene.add_child(timer) + else: + # Reset transforms to default when no transforms are specified + node.scale = Vector2.ONE + node.rotation = 0.0 + if node.has_meta("css_transform_applied"): + node.remove_meta("css_transform_applied") + +static func _reapply_transforms(node: Control) -> void: + if not is_instance_valid(node): + return + + if node.has_meta("pending_scale"): + node.scale = node.get_meta("pending_scale") + + if node.has_meta("pending_rotation"): + node.rotation = node.get_meta("pending_rotation") + + if node.has_meta("pending_scale"): + node.remove_meta("pending_scale") + if node.has_meta("pending_rotation"): + node.remove_meta("pending_rotation") diff --git a/flumi/Scripts/Utils/BackgroundUtils.gd b/flumi/Scripts/Utils/BackgroundUtils.gd index de9d2d2..c02ad79 100644 --- a/flumi/Scripts/Utils/BackgroundUtils.gd +++ b/flumi/Scripts/Utils/BackgroundUtils.gd @@ -182,6 +182,8 @@ static func setup_panel_hover_support(panel: PanelContainer, normal_styles: Dict # Store references for the hover handlers panel.set_meta("normal_stylebox", normal_stylebox) panel.set_meta("hover_stylebox", hover_stylebox) + panel.set_meta("normal_styles", normal_styles.duplicate(true)) + panel.set_meta("hover_styles", merged_hover_styles.duplicate(true)) # Connect mouse events panel.mouse_entered.connect(_on_panel_mouse_entered.bind(panel)) @@ -191,11 +193,28 @@ static func _on_panel_mouse_entered(panel: PanelContainer): if panel.has_meta("hover_stylebox"): var hover_stylebox = panel.get_meta("hover_stylebox") panel.add_theme_stylebox_override("panel", hover_stylebox) + + if panel.has_meta("hover_styles"): + var hover_styles = panel.get_meta("hover_styles") + var transform_target = find_transform_target_for_panel(panel) + StyleManager.apply_transform_properties_direct(transform_target, hover_styles) static func _on_panel_mouse_exited(panel: PanelContainer): if panel.has_meta("normal_stylebox"): var normal_stylebox = panel.get_meta("normal_stylebox") panel.add_theme_stylebox_override("panel", normal_stylebox) + + if panel.has_meta("normal_styles"): + var normal_styles = panel.get_meta("normal_styles") + var transform_target = find_transform_target_for_panel(panel) + StyleManager.apply_transform_properties_direct(transform_target, normal_styles) + +static func find_transform_target_for_panel(panel: PanelContainer) -> Control: + var parent = panel.get_parent() + if parent and parent is FlexContainer: + return parent + + return panel static func needs_background_wrapper(styles: Dictionary) -> bool: return styles.has("background-color") or styles.has("border-radius") or styles.has("padding") or styles.has("padding-top") or styles.has("padding-right") or styles.has("padding-bottom") or styles.has("padding-left") or styles.has("border-width") or styles.has("border-top-width") or styles.has("border-right-width") or styles.has("border-bottom-width") or styles.has("border-left-width") or styles.has("border-color") or styles.has("border-style") or styles.has("border-top-color") or styles.has("border-right-color") or styles.has("border-bottom-color") or styles.has("border-left-color") diff --git a/flumi/Scripts/Utils/TransformUtils.gd b/flumi/Scripts/Utils/TransformUtils.gd new file mode 100644 index 0000000..3c4436f --- /dev/null +++ b/flumi/Scripts/Utils/TransformUtils.gd @@ -0,0 +1,158 @@ +class_name TransformUtils +extends RefCounted + +# Utility functions for parsing CSS transform values (scale and rotation) + +# Parse scale values - returns a Vector2 with x and y scale factors +static func parse_scale(val: String) -> Vector2: + if val == null or val.is_empty(): + return Vector2.ONE + + # Named scale values + var named = { + "0": Vector2.ZERO, + "50": Vector2(0.5, 0.5), + "75": Vector2(0.75, 0.75), + "90": Vector2(0.9, 0.9), + "95": Vector2(0.95, 0.95), + "100": Vector2.ONE, + "105": Vector2(1.05, 1.05), + "110": Vector2(1.1, 1.1), + "125": Vector2(1.25, 1.25), + "150": Vector2(1.5, 1.5), + "200": Vector2(2.0, 2.0) + } + + if named.has(val): + return named[val] + + # Direct numeric value (treat as percentage: 100 = 1.0, 50 = 0.5, etc.) + if val.is_valid_int(): + var scale_factor = float(val) / 100.0 + return Vector2(scale_factor, scale_factor) + + # Direct decimal value + if val.is_valid_float(): + var scale_factor = float(val) + return Vector2(scale_factor, scale_factor) + + return Vector2.ONE + +# Parse rotation values - returns rotation in radians +static func parse_rotation(val: String) -> float: + if val == null or val.is_empty(): + return 0.0 + + # Named rotation values (in degrees, converted to radians) + var named = { + "0": 0.0, + "1": deg_to_rad(1.0), + "2": deg_to_rad(2.0), + "3": deg_to_rad(3.0), + "6": deg_to_rad(6.0), + "12": deg_to_rad(12.0), + "45": deg_to_rad(45.0), + "90": deg_to_rad(90.0), + "180": deg_to_rad(180.0), + "270": deg_to_rad(270.0) + } + + if named.has(val): + return named[val] + + # Handle explicit unit specifications + if val.ends_with("deg"): + var degrees = float(val.replace("deg", "")) + return deg_to_rad(degrees) + elif val.ends_with("rad"): + return float(val.replace("rad", "")) + elif val.ends_with("turn"): + var turns = float(val.replace("turn", "")) + return turns * 2.0 * PI + + # Direct numeric value - assume degrees + if val.is_valid_float(): + return deg_to_rad(float(val)) + + return 0.0 + +# Parse arbitrary value from brackets like [1.7] or [45deg] or [3.5rad] +static func parse_bracket_value(bracket_content: String, value_type: String): + if bracket_content.is_empty(): + return null + + match value_type: + "scale": + return parse_scale(bracket_content) + "rotation": + return parse_rotation(bracket_content) + _: + return null + +# Extract bracket content for transform utilities +static func extract_bracket_content(string: String, start_idx: int) -> String: + var open_idx = string.find("[", start_idx) + if open_idx == -1: + return "" + var close_idx = string.find("]", open_idx) + if close_idx == -1: + return "" + return string.substr(open_idx + 1, close_idx - open_idx - 1) + +# Parse scale utility and return the appropriate property name and value +static func parse_scale_utility(utility_name: String) -> Dictionary: + var result = {} + + if utility_name.begins_with("scale-x-"): + var val = utility_name.substr(8) # after "scale-x-" + if val.begins_with("[") and val.ends_with("]"): + val = val.substr(1, val.length() - 2) + var scale_vec = parse_scale(val) + result["scale-x"] = scale_vec.x + return result + elif utility_name.begins_with("scale-y-"): + var val = utility_name.substr(8) # after "scale-y-" + if val.begins_with("[") and val.ends_with("]"): + val = val.substr(1, val.length() - 2) + var scale_vec = parse_scale(val) + result["scale-y"] = scale_vec.y + return result + elif utility_name.begins_with("scale-"): + var val = utility_name.substr(6) # after "scale-" + if val.begins_with("[") and val.ends_with("]"): + val = val.substr(1, val.length() - 2) + var scale_vec = parse_scale(val) + result["scale-x"] = scale_vec.x + result["scale-y"] = scale_vec.y + return result + + return result + +# Parse rotation utility and return the appropriate property name and value +static func parse_rotation_utility(utility_name: String) -> Dictionary: + var result = {} + + if utility_name.begins_with("rotate-x-"): + var val = utility_name.substr(9) # after "rotate-x-" + if val.begins_with("[") and val.ends_with("]"): + val = val.substr(1, val.length() - 2) + var rotation = parse_rotation(val) + result["rotate-x"] = rotation + return result + elif utility_name.begins_with("rotate-y-"): + var val = utility_name.substr(9) # after "rotate-y-" + if val.begins_with("[") and val.ends_with("]"): + val = val.substr(1, val.length() - 2) + var rotation = parse_rotation(val) + result["rotate-y"] = rotation + return result + elif utility_name.begins_with("rotate-"): + var val = utility_name.substr(7) # after "rotate-" + if val.begins_with("[") and val.ends_with("]"): + val = val.substr(1, val.length() - 2) + var rotation = parse_rotation(val) + result["rotate-z"] = rotation # Default rotation is around Z-axis + return result + + return result + diff --git a/flumi/Scripts/Utils/TransformUtils.gd.uid b/flumi/Scripts/Utils/TransformUtils.gd.uid new file mode 100644 index 0000000..22c1767 --- /dev/null +++ b/flumi/Scripts/Utils/TransformUtils.gd.uid @@ -0,0 +1 @@ +uid://m8sb57jskjyp diff --git a/flumi/Scripts/Utils/UtilityClassValidator.gd b/flumi/Scripts/Utils/UtilityClassValidator.gd index 2a418e9..7817cad 100644 --- a/flumi/Scripts/Utils/UtilityClassValidator.gd +++ b/flumi/Scripts/Utils/UtilityClassValidator.gd @@ -41,6 +41,10 @@ static func init_patterns(): "^opacity-\\[.*\\]$", # custom opacity values "^z-\\[.*\\]$", # custom z-index values "^cursor-[a-zA-Z-]+$", # cursor types + "^scale-(x-|y-)?\\d+$", # scale utilities like scale-100, scale-x-75, scale-y-150 + "^scale-(x-|y-)?\\[.*\\]$", # custom scale values like scale-[1.5], scale-x-[2.0] + "^rotate-(x-|y-)?\\d+$", # rotation utilities like rotate-45, rotate-x-90, rotate-y-180 + "^rotate-(x-|y-)?\\[.*\\]$", # custom rotation values like rotate-[45deg], rotate-x-[90deg], rotate-y-[3.5rad] "^(hover|active):", # pseudo classes ] for pattern in utility_patterns: