feat(iced): port full DSL parser and executor to ICED

This commit is contained in:
2026-07-15 16:37:04 +03:00
parent ba6a7f8de2
commit 2646d962d2
4 changed files with 1716 additions and 0 deletions
@@ -0,0 +1,306 @@
//! DSL script executor for the HCIE-Rust ICED GUI.
//!
//! Translates parsed `ScriptAction` values into `hcie_engine_api::Engine`
//! calls. This module handles:
//!
//! - Hex color conversion to `[u8; 4]` RGBA
//! - Blend mode string parsing to `BlendMode` enum
//! - Brush style string parsing to `BrushStyle` enum
//! - Layer lookup by name or index
//! - Stroke drawing with per-point pressure
//! - Raster rectangle fills
//! - Vector shape creation
use crate::script::parser::ScriptAction;
use hcie_engine_api::Engine;
use hcie_engine_api::{BlendMode, BrushStyle, BrushTip};
/// Converts a hex color string to `[r, g, b, a]` byte array.
///
/// Supports both 6-character (`#RRGGBB`) and 8-character (`#RRGGBBAA`) formats.
/// Defaults alpha to 255 if not specified.
///
/// # Arguments
///
/// * `hex` — The hex color string (leading `#` is stripped).
///
/// # Returns
///
/// A 4-element array `[r, g, b, a]` with values in `[0, 255]`.
fn hex_to_rgba(hex: &str) -> [u8; 4] {
let hex = hex.trim_start_matches('#');
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
let a = if hex.len() >= 8 {
u8::from_str_radix(&hex[6..8], 16).unwrap_or(255)
} else {
255
};
[r, g, b, a]
}
/// Parses a blend mode string into the corresponding `BlendMode` enum variant.
///
/// Accepts both camelCase and snake_case forms (e.g. `"colorBurn"` or
/// `"color_burn"`). Defaults to `Normal` for unrecognized strings.
///
/// # Arguments
///
/// * `s` — The blend mode name string.
///
/// # Returns
///
/// The matching `BlendMode` variant.
fn parse_blend_mode(s: &str) -> BlendMode {
match s.to_lowercase().as_str() {
"normal" => BlendMode::Normal,
"dissolve" => BlendMode::Dissolve,
"darken" => BlendMode::Darken,
"multiply" => BlendMode::Multiply,
"colorburn" | "color_burn" => BlendMode::ColorBurn,
"linearburn" | "linear_burn" => BlendMode::LinearBurn,
"darker" | "darkercolor" => BlendMode::DarkerColor,
"lighten" => BlendMode::Lighten,
"screen" => BlendMode::Screen,
"colordodge" | "color_dodge" => BlendMode::ColorDodge,
"lineardodge" | "linear_dodge" => BlendMode::LinearDodge,
"lighter" | "lightercolor" => BlendMode::LighterColor,
"overlay" => BlendMode::Overlay,
"softlight" | "soft_light" => BlendMode::SoftLight,
"hardlight" | "hard_light" => BlendMode::HardLight,
"vividlight" | "vivid_light" => BlendMode::VividLight,
"linearlight" | "linear_light" => BlendMode::LinearLight,
"pinlight" | "pin_light" => BlendMode::PinLight,
"hardmix" | "hard_mix" => BlendMode::HardMix,
"difference" => BlendMode::Difference,
"exclusion" => BlendMode::Exclusion,
"subtract" => BlendMode::Subtract,
"divide" => BlendMode::Divide,
"hue" => BlendMode::Hue,
"saturation" => BlendMode::Saturation,
"color" => BlendMode::Color,
"luminosity" => BlendMode::Luminosity,
_ => BlendMode::Normal,
}
}
/// Parses a brush style string into the corresponding `BrushStyle` enum variant.
///
/// Accepts various aliases (e.g. `"soft_round"`, `"inkpen"`, `"wet_paint"`).
/// Defaults to `Round` for unrecognized strings.
///
/// # Arguments
///
/// * `s` — The brush style name string.
///
/// # Returns
///
/// The matching `BrushStyle` variant.
fn parse_brush_style(s: &str) -> BrushStyle {
match s.to_lowercase().as_str() {
"default" | "round" => BrushStyle::Round,
"hardround" => BrushStyle::HardRound,
"softround" | "soft_round" => BrushStyle::SoftRound,
"square" => BrushStyle::Square,
"pencil" => BrushStyle::Pencil,
"inkpen" | "pen" => BrushStyle::InkPen,
"charcoal" => BrushStyle::Charcoal,
"watercolor" => BrushStyle::Watercolor,
"marker" => BrushStyle::Marker,
"glow" => BrushStyle::Glow,
"airbrush" => BrushStyle::Airbrush,
"spray" => BrushStyle::Spray,
"oil" => BrushStyle::Oil,
"tree" => BrushStyle::Tree,
"meadow" => BrushStyle::Meadow,
"rock" => BrushStyle::Rock,
"clouds" => BrushStyle::Clouds,
"dirt" => BrushStyle::Dirt,
"bristle" => BrushStyle::Bristle,
"crayon" => BrushStyle::Crayon,
"leaf" => BrushStyle::Leaf,
"wetpaint" | "wet_paint" => BrushStyle::WetPaint,
"sketch" => BrushStyle::Sketch,
"hatch" => BrushStyle::Hatch,
"calligraphy" => BrushStyle::Calligraphy,
"blender" => BrushStyle::Blender,
"mixer" => BrushStyle::Mixer,
_ => BrushStyle::Round,
}
}
/// Finds a layer ID by name or numeric index.
///
/// First attempts to parse `name_or_index` as a `usize` index into the
/// layer list. If that fails, searches layer names for an exact match.
///
/// # Arguments
///
/// * `engine` — The engine instance to query.
/// * `name_or_index` — The layer name or zero-based index string.
///
/// # Returns
///
/// The layer `u64` ID if found, otherwise `None`.
fn find_layer_id(engine: &mut Engine, name_or_index: &str) -> Option<u64> {
if let Ok(idx) = name_or_index.parse::<usize>() {
let ids = engine.get_all_layer_ids();
return ids.get(idx).copied();
}
let infos = engine.layer_infos();
for info in &infos {
if info.name == name_or_index {
return Some(info.id);
}
}
None
}
/// Executes a sequence of parsed `ScriptAction` values against the engine.
///
/// Iterates through actions in order, translating each into the appropriate
/// engine API call. Layer operations, brush settings, drawing commands, and
/// vector shapes are all handled here.
///
/// # Arguments
///
/// * `engine` — The mutable engine instance to operate on.
/// * `actions` — The slice of actions to execute.
pub fn execute_actions(engine: &mut Engine, actions: &[ScriptAction]) {
for action in actions {
match action {
ScriptAction::CreateLayer { name, layer_type: _ } => {
engine.add_layer(name);
}
ScriptAction::SelectLayer { name_or_index } => {
if let Some(id) = find_layer_id(engine, name_or_index) {
engine.set_active_layer(id);
}
}
ScriptAction::SetLayerOpacity { name_or_index, opacity } => {
if let Some(id) = find_layer_id(engine, name_or_index) {
engine.set_layer_opacity(id, *opacity);
}
}
ScriptAction::SetLayerBlendMode { name_or_index, blend_mode } => {
if let Some(id) = find_layer_id(engine, name_or_index) {
let bm = parse_blend_mode(blend_mode);
engine.set_layer_blend_mode(id, bm);
}
}
ScriptAction::SetForegroundColor { hex } => {
let rgba = hex_to_rgba(hex);
engine.set_color(rgba);
}
ScriptAction::SetBrush {
style,
size,
opacity,
spacing,
hardness,
} => {
let mut tip = BrushTip::default();
if let Some(s) = style {
tip.style = parse_brush_style(s);
}
if let Some(sz) = size {
tip.size = *sz;
}
if let Some(o) = opacity {
tip.opacity = *o;
}
if let Some(sp) = spacing {
tip.spacing = *sp;
}
if let Some(h) = hardness {
tip.hardness = *h;
}
engine.set_brush_tip(tip);
}
ScriptAction::PaintStroke {
points,
color,
brush_style,
size,
opacity,
spacing,
hardness,
} => {
let rgba = hex_to_rgba(color);
engine.set_color(rgba);
let mut tip = BrushTip::default();
tip.style = parse_brush_style(brush_style);
tip.size = *size;
tip.opacity = *opacity;
tip.spacing = *spacing;
tip.hardness = *hardness;
engine.set_brush_tip(tip);
let stroke_points: Vec<(f32, f32, f32)> = points
.iter()
.map(|p| (p[0], p[1], 1.0))
.collect();
engine.draw_stroke(&stroke_points);
}
ScriptAction::FillRasterRect {
x1,
y1,
x2,
y2,
color,
} => {
let rgba = hex_to_rgba(color);
let (cw, ch) = engine.get_canvas_size();
let dx1 = (x1 * cw as f32) as u32;
let dy1 = (y1 * ch as f32) as u32;
let dx2 = (x2 * cw as f32) as u32;
let dy2 = (y2 * ch as f32) as u32;
engine.draw_filled_rect_rgba(dx1, dy1, dx2, dy2, rgba);
}
ScriptAction::AddVectorPath {
points,
closed,
fill_color,
stroke_color,
stroke_width,
} => {
let path_points: Vec<[f32; 2]> = points.clone();
let fc = [
hex_to_rgba(fill_color)[0],
hex_to_rgba(fill_color)[1],
hex_to_rgba(fill_color)[2],
255,
];
let sc = [
hex_to_rgba(stroke_color)[0],
hex_to_rgba(stroke_color)[1],
hex_to_rgba(stroke_color)[2],
255,
];
engine.add_vector_shape(hcie_engine_api::VectorShape::FreePath {
pts: path_points,
closed: *closed,
stroke: *stroke_width,
color: sc,
fill_color: fc,
fill: true,
opacity: 1.0,
hardness: 0.8,
});
}
ScriptAction::Finish { .. } => {}
ScriptAction::DeleteLayer { name_or_index } => {
if let Some(id) = find_layer_id(engine, name_or_index) {
engine.delete_layer(id);
}
}
ScriptAction::RenameLayer { name_or_index, new_name } => {
if let Some(id) = find_layer_id(engine, name_or_index) {
engine.rename_layer(id, new_name);
}
}
}
}
}
@@ -0,0 +1,5 @@
/// DSL parser module — tokenizer, recursive-descent parser, and action types
/// for the HCIE-Rust scripting engine.
pub mod types;
pub mod parser;
pub mod executor;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
use serde::{Deserialize, Serialize};
/// Persistent parser state for the DSL script interpreter.
///
/// Tracks the current drawing context — active brush settings, color,
/// layer properties, variables, and pressure mapping — across lines
/// of a parsed DSL script. Each `repeat` block forks a copy of this
/// state so inner iterations can override values without leaking back
/// to the outer scope.
///
/// # Fields
///
/// * `color` — Current foreground color as a hex string (e.g. `"#FF0000"`).
/// * `style` — Current brush style name (e.g. `"round"`, `"watercolor"`).
/// * `size` — Current brush size in pixels.
/// * `opacity` — Current drawing opacity, clamped to `[0.0, 1.0]`.
/// * `hardness` — Current brush hardness, clamped to `[0.0, 1.0]`.
/// * `spacing` — Brush spacing factor derived from the `flow` setting.
/// * `blend_mode` — Current layer blend mode as a lowercase string.
/// * `vars` — User-defined variables accessible via `$name` syntax.
/// * `scatter_size` — Brush size used by the `scatter` command.
/// * `flow` — Brush flow value (higher = denser strokes).
/// * `pressure` — Uniform pen pressure multiplier.
/// * `pressure_start` — Pressure at stroke start for tapering.
/// * `pressure_end` — Pressure at stroke end for tapering.
/// * `wiggle` — Random displacement amplitude applied to stroke points.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptParserState {
pub color: String,
pub style: String,
pub size: f32,
pub opacity: f32,
pub hardness: f32,
pub spacing: f32,
pub blend_mode: String,
pub vars: std::collections::HashMap<String, f32>,
pub scatter_size: f32,
pub flow: f32,
pub pressure: f32,
pub pressure_start: f32,
pub pressure_end: f32,
pub wiggle: f32,
}
impl Default for ScriptParserState {
fn default() -> Self {
Self {
color: "#000000".to_string(),
style: "round".to_string(),
size: 20.0,
opacity: 1.0,
hardness: 0.8,
spacing: 0.1,
blend_mode: "normal".to_string(),
vars: std::collections::HashMap::new(),
scatter_size: 12.0,
flow: 0.5,
pressure: 1.0,
pressure_start: 1.0,
pressure_end: 1.0,
wiggle: 0.0,
}
}
}