feat(iced): port full DSL parser and executor to ICED
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user