diff --git a/build.rs b/build.rs index 97135da56..fd50c581c 100644 --- a/build.rs +++ b/build.rs @@ -4,7 +4,6 @@ use std::path::Path; fn main() { - generate_draw_panel_tools(); let version = std::env::var("CARGO_PKG_VERSION").expect("Cargo package version"); let parts: Vec<&str> = version.split('.').collect(); let app_version = if parts.len() == 3 && parts[0].len() == 4 @@ -82,27 +81,3 @@ fn main() { } } } - -/// Each drawing button can contribute its own expression without editing a registry. -fn generate_draw_panel_tools() { - let manifest = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").expect("Cargo manifest directory")); - let directory = manifest.join("src/ui/ribbon/draw_tools"); - println!("cargo:rerun-if-changed={}", directory.display()); - let mut files = match std::fs::read_dir(&directory) { - Ok(entries) => entries.map(|entry| entry.expect("Draw tool directory entry").path()) - .filter(|path| path.is_file() && path.extension().is_some_and(|extension| extension == "rs")) - .collect::>(), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Vec::new(), - Err(error) => panic!("Cannot read Draw tool contributions: {error}"), - }; - files.sort_by(|left, right| left.file_name().cmp(&right.file_name())); - let mut source = String::from("const TOOLS: &[Tool] = &[\n"); - for path in files { - println!("cargo:rerun-if-changed={}", path.display()); - // Debug string formatting produces a quoted Rust literal, including Windows escapes. - source.push_str(&format!(" include!({:?}),\n", path.to_str().expect("UTF-8 Draw tool path"))); - } - source.push_str("];\n"); - let output = std::path::PathBuf::from(std::env::var_os("OUT_DIR").expect("Cargo output directory")); - std::fs::write(output.join("draw_panel_tools.rs"), source).expect("Write Draw tool registry"); -} diff --git a/src/modules/draw/draw/ray.rs b/src/modules/draw/draw/ray.rs index 30d7b3f6b..6318fde7f 100644 --- a/src/modules/draw/draw/ray.rs +++ b/src/modules/draw/draw/ray.rs @@ -4,10 +4,10 @@ // Produces a Ray entity; repeats until Enter/Esc. // XLINE — infinite construction line: same two-click pattern, yields XLine. +use crate::t; use acadrust::entities::{Ray as RayEnt, XLine as XLineEnt}; use acadrust::types::Vector3; use acadrust::EntityType; -use crate::t; use crate::command::{CadCommand, CmdResult}; use crate::scene::model::wire_model::WireModel; @@ -226,7 +226,34 @@ impl CadCommand for XLineCommand { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn repeated_rays_and_xlines_keep_their_first_point() { + let base = DVec3::new(2.0, 3.0, 4.0); + let mut ray = RayCommand::new(); + let mut xline = XLineCommand::new(); + assert!(matches!(ray.on_point(base), CmdResult::NeedPoint)); + assert!(matches!(xline.on_point(base), CmdResult::NeedPoint)); + + for through in [DVec3::new(3.0, 3.0, 4.0), DVec3::new(2.0, 5.0, 4.0)] { + let CmdResult::CommitEntity(EntityType::Ray(entity)) = ray.on_point(through) else { + panic!("ray was not committed"); + }; + assert_eq!(entity.base_point, Vector3::new(base.x, base.y, base.z)); + + let CmdResult::CommitEntity(EntityType::XLine(entity)) = xline.on_point(through) else { + panic!("construction line was not committed"); + }; + assert_eq!(entity.base_point, Vector3::new(base.x, base.y, base.z)); + } + } +} // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["RAY"] }); // RayCommand -inventory::submit!(crate::command::CommandRegistration { names: &["CONSTRUCTIONLINE", "XLINE"] }); // XLineCommand +inventory::submit!(crate::command::CommandRegistration { names: &["RAY"] }); // RayCommand +inventory::submit!(crate::command::CommandRegistration { + names: &["CONSTRUCTIONLINE", "XLINE"] +}); // XLineCommand diff --git a/src/modules/draw/draw/spline.rs b/src/modules/draw/draw/spline.rs index f143b638b..8ace3cb2d 100644 --- a/src/modules/draw/draw/spline.rs +++ b/src/modules/draw/draw/spline.rs @@ -3,9 +3,9 @@ // Command: SPLINE (SPL) // Click to add fit points. Enter (≥2 pts) → commits EntityType::Spline. +use crate::t; use acadrust::types::Vector3; use acadrust::{EntityType, Spline}; -use crate::t; use crate::command::{CadCommand, CmdResult}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; @@ -30,27 +30,35 @@ pub struct SplineCommand { impl SplineCommand { pub fn new() -> Self { - Self { pts: Vec::new(), control_vertices: false, choosing_method: false } + Self { + pts: Vec::new(), + control_vertices: false, + choosing_method: false, + } } pub fn control_vertices() -> Self { - Self { control_vertices: true, ..Self::new() } + Self { + control_vertices: true, + ..Self::new() + } } fn build(&self, closed: bool) -> Option { if self.pts.len() < 2 { return None; } - Some(EntityType::Spline(make_spline(&self.pts, closed, self.control_vertices))) + Some(EntityType::Spline(make_spline( + &self.pts, + closed, + self.control_vertices, + ))) } } /// Store the chosen construction method directly in the persistent spline. fn make_spline(pts: &[DVec3], closed: bool, control_vertices: bool) -> Spline { - let mut points: Vec = pts - .iter() - .map(|p| Vector3::new(p.x, p.y, p.z)) - .collect(); + let mut points: Vec = pts.iter().map(|p| Vector3::new(p.x, p.y, p.z)).collect(); let mut spline = if control_vertices { if closed && pts.first() != pts.last() { points.push(points[0]); @@ -61,9 +69,13 @@ fn make_spline(pts: &[DVec3], closed: bool, control_vertices: bool) -> Spline { // Open uniform knot vector: degree + 1 equal knots at each endpoint. let knots = (0..count + degree + 1) .map(|index| { - if index <= degree { 0.0 } - else if index >= count { 1.0 } - else { (index - degree) as f64 / spans as f64 } + if index <= degree { + 0.0 + } else if index >= count { + 1.0 + } else { + (index - degree) as f64 / spans as f64 + } }) .collect(); Spline { @@ -103,7 +115,11 @@ fn sample_curve(pts: &[DVec3], closed: bool, control_vertices: bool) -> Vec<[f32 impl CadCommand for SplineCommand { fn name(&self) -> &'static str { - if self.control_vertices { "SPLINECV" } else { "SPLINE" } + if self.control_vertices { + "SPLINECV" + } else { + "SPLINE" + } } fn prompt(&self) -> String { @@ -122,7 +138,10 @@ impl CadCommand for SplineCommand { fn options(&self) -> Vec { use crate::command::CmdOption; if self.choosing_method { - return vec![CmdOption::new("Fit", "FIT"), CmdOption::new("Control vertices", "CV")]; + return vec![ + CmdOption::new("Fit", "FIT"), + CmdOption::new("Control vertices", "CV"), + ]; } if self.pts.is_empty() { return vec![CmdOption::new(t!("Method").as_ref(), "M")]; @@ -213,6 +232,32 @@ impl CadCommand for SplineCommand { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn control_vertices_make_a_clamped_finite_spline() { + let points = [ + DVec3::new(0.0, 0.0, 0.0), + DVec3::new(1.0, 2.0, 0.0), + DVec3::new(2.0, 2.0, 0.0), + DVec3::new(3.0, 0.0, 0.0), + ]; + let spline = make_spline(&points, false, true); + + assert_eq!(spline.degree, 3); + assert_eq!(spline.control_points.len(), 4); + assert_eq!(spline.knots, vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]); + assert_eq!(spline.weights, vec![1.0; 4]); + assert!(sample_curve(&points, false, true) + .iter() + .flatten() + .all(|value| value.is_finite())); + } +} // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["SPLINE", "SPLINECV"] }); // SplineCommand +inventory::submit!(crate::command::CommandRegistration { + names: &["SPLINE", "SPLINECV"] +}); // SplineCommand diff --git a/src/ui/ribbon/draw_panel.rs b/src/ui/ribbon/draw_panel.rs index 4e8e78614..2f8267d70 100644 --- a/src/ui/ribbon/draw_panel.rs +++ b/src/ui/ribbon/draw_panel.rs @@ -5,7 +5,10 @@ use std::time::Duration; use iced::widget::{button, column, container, row, scrollable, text, tooltip}; use iced::{Element, Fill, Theme}; -use super::widgets::{make_icon, make_tip, muted_text_style, popup_panel_style, popup_row_style, tip_style, tool_btn_style}; +use super::widgets::{ + make_icon, make_tip, muted_text_style, popup_panel_style, popup_row_style, tip_style, + tool_btn_style, +}; use super::{dropdown_backdrop, position_ribbon_dropdown, Ribbon}; use crate::app::Message; use crate::modules::IconKind; @@ -23,39 +26,158 @@ struct Tool { options: &'static [(&'static str, &'static str)], } -include!(concat!(env!("OUT_DIR"), "/draw_panel_tools.rs")); +const TOOLS: &[Tool] = &[ + Tool { + command: "SPLINE", + label: "Spline Fit", + icon: include_bytes!("../../../assets/icons/spline.svg"), + options: &[], + }, + Tool { + command: "SPLINECV", + label: "Spline CV", + icon: include_bytes!("../../../assets/icons/spline_cv.svg"), + options: &[], + }, + Tool { + command: "XLINE", + label: "Construction Line", + icon: include_bytes!("../../../assets/icons/xline.svg"), + options: &[], + }, + Tool { + command: "RAY", + label: "Ray", + icon: include_bytes!("../../../assets/icons/ray.svg"), + options: &[], + }, + Tool { + command: "MULTIPOINT", + label: "Multiple Points", + icon: include_bytes!("../../../assets/icons/multipoint.svg"), + options: &[ + ("POINT", "Single Point"), + ("MULTIPOINT", "Multiple Points"), + ("DDPTYPE", "Point Style"), + ], + }, + Tool { + command: "DIVIDE", + label: "Divide", + icon: include_bytes!("../../../assets/icons/divide.svg"), + options: &[], + }, + Tool { + command: "MEASURE", + label: "Measure", + icon: include_bytes!("../../../assets/icons/measure.svg"), + options: &[], + }, + Tool { + command: "REGION", + label: "Region", + icon: include_bytes!("../../../assets/icons/region.svg"), + options: &[], + }, + Tool { + command: "WIPEOUT", + label: "Wipeout", + icon: include_bytes!("../../../assets/icons/wipeout.svg"), + options: &[], + }, + Tool { + command: "BOUNDARY", + label: "Boundary", + icon: include_bytes!("../../../assets/icons/boundary.svg"), + options: &[], + }, + Tool { + command: "HELIX", + label: "Helix", + icon: include_bytes!("../../../assets/icons/helix.svg"), + options: &[], + }, + Tool { + command: "DONUT", + label: "Donut", + icon: include_bytes!("../../../assets/icons/donut.svg"), + options: &[], + }, + Tool { + command: "REVCLOUD", + label: "Revision Cloud", + icon: include_bytes!("../../../assets/icons/revcloud.svg"), + options: &[ + ("REVCLOUD_RECTANGULAR", "Rectangular"), + ("REVCLOUD_POLYGONAL", "Polygonal"), + ("REVCLOUD_FREEHAND", "Freehand"), + ], + }, +]; pub(super) fn owns_dropdown(id: &str) -> bool { - id == PANEL_ID || TOOLS.iter().any(|tool| tool.command == id && !tool.options.is_empty()) + id == PANEL_ID + || TOOLS + .iter() + .any(|tool| tool.command == id && !tool.options.is_empty()) } pub(super) fn group_title<'a>(title: &'static str, open: &Option) -> Element<'a, Message> { if title != "Draw" || TOOLS.is_empty() { - return container(text(t!(title)).size(9).style(muted_text_style)).padding([1, 4]).into(); + return container(text(t!(title)).size(9).style(muted_text_style)) + .padding([1, 4]) + .into(); } let expanded = open.as_deref().is_some_and(owns_dropdown); - let arrow = if expanded { icons::themed_arrow_up(7.0) } else { icons::themed_arrow_down(7.0) }; - PosReport::new(PANEL_ID, button(row![text(t!(title)).size(9), arrow].spacing(4).align_y(iced::Center)) + let arrow = if expanded { + icons::themed_arrow_up(7.0) + } else { + icons::themed_arrow_down(7.0) + }; + PosReport::new( + PANEL_ID, + button( + row![text(t!(title)).size(9), arrow] + .spacing(4) + .align_y(iced::Center), + ) .on_press(Message::ToggleRibbonDropdown(PANEL_ID.to_string())) .style(move |theme: &Theme, status| tool_btn_style(theme, expanded, status)) - .padding([1, 4])).into() + .padding([1, 4]), + ) + .into() } fn tool_button(tool: &Tool, active: bool) -> Element<'static, Message> { let face = button(make_icon(IconKind::Svg(tool.icon), 23.0)) - .on_press(Message::DropdownSelectItem { dropdown_id: PANEL_ID, cmd: tool.command }) + .on_press(Message::DropdownSelectItem { + dropdown_id: PANEL_ID, + cmd: tool.command, + }) .style(move |theme: &Theme, status| tool_btn_style(theme, active, status)) - .width(if tool.options.is_empty() { CELL } else { CELL - 11.0 }) + .width(if tool.options.is_empty() { + CELL + } else { + CELL - 11.0 + }) .height(CELL) .padding(3); let tip = format!("{}\n{} {}", t!(tool.label), t!("Command:"), tool.command); let face: Element<'static, Message> = tooltip(face, make_tip(tip), tooltip::Position::Bottom) - .delay(Duration::from_millis(400)).style(tip_style).into(); + .delay(Duration::from_millis(400)) + .style(tip_style) + .into(); if !tool.options.is_empty() { - row![face, button(icons::themed_arrow_down(6.0)) - .on_press(Message::ToggleRibbonDropdown(tool.command.to_string())) - .style(move |theme: &Theme, status| tool_btn_style(theme, false, status)) - .width(11).height(CELL).padding(2)].into() + row![ + face, + button(icons::themed_arrow_down(6.0)) + .on_press(Message::ToggleRibbonDropdown(tool.command.to_string())) + .style(move |theme: &Theme, status| tool_btn_style(theme, false, status)) + .width(11) + .height(CELL) + .padding(2) + ] + .into() } else { face } @@ -65,28 +187,60 @@ pub(super) fn overlay<'a>(ribbon: &Ribbon, id: &str, win: (f32, f32)) -> Element let width = (7.0 * (CELL + GAP) - GAP + 12.0).min((win.0 - 8.0).max(CELL + 12.0)); let (_, x, anchor_top) = ribbon.dd_anchor(PANEL_ID, width, win.0); let anchor = crate::ui::wrap_bar::dropdown_bounds(PANEL_ID); - let left = anchor.map_or(x, |b| b.x).clamp(0.0, (win.0 - width).max(0.0)); + let left = anchor + .map_or(x, |b| b.x) + .clamp(0.0, (win.0 - width).max(0.0)); let top = anchor_top.min((win.1 - 80.0).max(0.0)); let available_height = (win.1 - top - 4.0).max(1.0); let cols = (((width - 12.0 + GAP) / (CELL + GAP)).floor() as usize).clamp(1, 7); - let option_tool = TOOLS.iter().find(|tool| tool.command == id && !tool.options.is_empty()); + let option_tool = TOOLS + .iter() + .find(|tool| tool.command == id && !tool.options.is_empty()); let content_height = option_tool.map_or_else( || TOOLS.len().div_ceil(cols) as f32 * (CELL + GAP) - GAP + 12.0, |tool| tool.options.len() as f32 * 30.0, ); let contents: Element<'static, Message> = if let Some(tool) = option_tool { - column(tool.options.iter().map(|&(cmd, label)| { - button(text(t!(label)).size(11)) - .on_press(Message::DropdownSelectItem { dropdown_id: tool.command, cmd }) - .style(popup_row_style).width(Fill).height(30).padding([6, 10]).into() - }).collect::>>()).into() + column( + tool.options + .iter() + .map(|&(cmd, label)| { + button(text(t!(label)).size(11)) + .on_press(Message::DropdownSelectItem { + dropdown_id: tool.command, + cmd, + }) + .style(popup_row_style) + .width(Fill) + .height(30) + .padding([6, 10]) + .into() + }) + .collect::>>(), + ) + .into() } else { - column(TOOLS.chunks(cols).map(|tools| { - row(tools.iter().map(|tool| tool_button(tool, ribbon.active_tool.as_deref() == Some(tool.command))) - .collect::>()).spacing(GAP).into() - }).collect::>>()).spacing(GAP).padding(6).into() + column( + TOOLS + .chunks(cols) + .map(|tools| { + row(tools + .iter() + .map(|tool| { + tool_button(tool, ribbon.active_tool.as_deref() == Some(tool.command)) + }) + .collect::>()) + .spacing(GAP) + .into() + }) + .collect::>>(), + ) + .spacing(GAP) + .padding(6) + .into() }; let panel = container(scrollable(contents).height(content_height.min(available_height))) - .width(width).style(popup_panel_style); + .width(width) + .style(popup_panel_style); dropdown_backdrop(position_ribbon_dropdown(panel.into(), false, left, top)) } diff --git a/src/ui/ribbon/draw_tools/01_spline_fit.rs b/src/ui/ribbon/draw_tools/01_spline_fit.rs deleted file mode 100644 index 7eae11258..000000000 --- a/src/ui/ribbon/draw_tools/01_spline_fit.rs +++ /dev/null @@ -1,6 +0,0 @@ -Tool { - command: "SPLINE", - label: "Spline Fit", - icon: include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons/spline.svg")), - options: &[], -} diff --git a/src/ui/ribbon/draw_tools/02_spline_cv.rs b/src/ui/ribbon/draw_tools/02_spline_cv.rs deleted file mode 100644 index ddb30c5e2..000000000 --- a/src/ui/ribbon/draw_tools/02_spline_cv.rs +++ /dev/null @@ -1,6 +0,0 @@ -Tool { - command: "SPLINECV", - label: "Spline CV", - icon: include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons/spline_cv.svg")), - options: &[], -} diff --git a/src/ui/ribbon/draw_tools/03_xline.rs b/src/ui/ribbon/draw_tools/03_xline.rs deleted file mode 100644 index 65e5138f8..000000000 --- a/src/ui/ribbon/draw_tools/03_xline.rs +++ /dev/null @@ -1,6 +0,0 @@ -Tool { - command: "XLINE", - label: "Construction Line", - icon: include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons/xline.svg")), - options: &[], -} diff --git a/src/ui/ribbon/draw_tools/04_ray.rs b/src/ui/ribbon/draw_tools/04_ray.rs deleted file mode 100644 index 3fc739731..000000000 --- a/src/ui/ribbon/draw_tools/04_ray.rs +++ /dev/null @@ -1,6 +0,0 @@ -Tool { - command: "RAY", - label: "Ray", - icon: include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons/ray.svg")), - options: &[], -} diff --git a/src/ui/ribbon/draw_tools/05_multipoint.rs b/src/ui/ribbon/draw_tools/05_multipoint.rs deleted file mode 100644 index c2a3445e6..000000000 --- a/src/ui/ribbon/draw_tools/05_multipoint.rs +++ /dev/null @@ -1,6 +0,0 @@ -Tool { - command: "MULTIPOINT", - label: "Multiple Points", - icon: include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons/multipoint.svg")), - options: &[("POINT", "Single Point"), ("MULTIPOINT", "Multiple Points"), ("DDPTYPE", "Point Style")], -} diff --git a/src/ui/ribbon/draw_tools/06_divide.rs b/src/ui/ribbon/draw_tools/06_divide.rs deleted file mode 100644 index 9ea3d44a3..000000000 --- a/src/ui/ribbon/draw_tools/06_divide.rs +++ /dev/null @@ -1,6 +0,0 @@ -Tool { - command: "DIVIDE", - label: "Divide", - icon: include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons/divide.svg")), - options: &[], -} diff --git a/src/ui/ribbon/draw_tools/07_measure.rs b/src/ui/ribbon/draw_tools/07_measure.rs deleted file mode 100644 index 359b1afc8..000000000 --- a/src/ui/ribbon/draw_tools/07_measure.rs +++ /dev/null @@ -1,6 +0,0 @@ -Tool { - command: "MEASURE", - label: "Measure", - icon: include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons/measure.svg")), - options: &[], -} diff --git a/src/ui/ribbon/draw_tools/08_region.rs b/src/ui/ribbon/draw_tools/08_region.rs deleted file mode 100644 index a09ec4dea..000000000 --- a/src/ui/ribbon/draw_tools/08_region.rs +++ /dev/null @@ -1,6 +0,0 @@ -Tool { - command: "REGION", - label: "Region", - icon: include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons/region.svg")), - options: &[], -} diff --git a/src/ui/ribbon/draw_tools/09_wipeout.rs b/src/ui/ribbon/draw_tools/09_wipeout.rs deleted file mode 100644 index ba3210262..000000000 --- a/src/ui/ribbon/draw_tools/09_wipeout.rs +++ /dev/null @@ -1,6 +0,0 @@ -Tool { - command: "WIPEOUT", - label: "Wipeout", - icon: include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons/wipeout.svg")), - options: &[], -} diff --git a/src/ui/ribbon/draw_tools/10_boundary.rs b/src/ui/ribbon/draw_tools/10_boundary.rs deleted file mode 100644 index 395ecb34f..000000000 --- a/src/ui/ribbon/draw_tools/10_boundary.rs +++ /dev/null @@ -1,6 +0,0 @@ -Tool { - command: "BOUNDARY", - label: "Boundary", - icon: include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons/boundary.svg")), - options: &[], -} diff --git a/src/ui/ribbon/draw_tools/11_helix.rs b/src/ui/ribbon/draw_tools/11_helix.rs deleted file mode 100644 index 844397765..000000000 --- a/src/ui/ribbon/draw_tools/11_helix.rs +++ /dev/null @@ -1,6 +0,0 @@ -Tool { - command: "HELIX", - label: "Helix", - icon: include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons/helix.svg")), - options: &[], -} diff --git a/src/ui/ribbon/draw_tools/12_donut.rs b/src/ui/ribbon/draw_tools/12_donut.rs deleted file mode 100644 index 3761f1b61..000000000 --- a/src/ui/ribbon/draw_tools/12_donut.rs +++ /dev/null @@ -1,6 +0,0 @@ -Tool { - command: "DONUT", - label: "Donut", - icon: include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons/donut.svg")), - options: &[], -} diff --git a/src/ui/ribbon/draw_tools/13_revcloud.rs b/src/ui/ribbon/draw_tools/13_revcloud.rs deleted file mode 100644 index cedb4a4f5..000000000 --- a/src/ui/ribbon/draw_tools/13_revcloud.rs +++ /dev/null @@ -1,10 +0,0 @@ -Tool { - command: "REVCLOUD", - label: "Revision Cloud", - icon: include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons/revcloud.svg")), - options: &[ - ("REVCLOUD_RECTANGULAR", "Rectangular"), - ("REVCLOUD_POLYGONAL", "Polygonal"), - ("REVCLOUD_FREEHAND", "Freehand"), - ], -}