graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: the wrangle's Code row applies on ctrl+enter, and a script error flags its line
Against cce-ui's new code editor (eeca5b7): edits stay in the row's
buffer until ctrl+enter, Escape or leaving the row, so a half-typed
script no longer fails the node on every keystroke. When an evaluation
error names the node the params pane shows and carries a `(line N`, the
line is handed to the pane, which paints it red in the gutter with a
band under the text; cleared on the next evaluation that says nothing
about that node. Rhai's line numbers survive desugar because the `@`
rewrite never adds or removes a line. The runner's undo / redo chords
reach the editor's own history ahead of the viewer tool's.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
CLAUDE.md | 12 ++++++++++++
shapeshifter.md | 8 +++++---
src/application.rs | 6 ++++--
src/main.rs | 21 +++++++++++++++++++++
src/render.rs | 31 +++++++++++++++++++++++++++++++
src/slots.rs | 6 ++++++
6 files changed, 79 insertions(+), 5 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 1132571..3d7f374 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -771,6 +771,18 @@ below native Rust, which is fine for tens of thousands of elements per edit
and wrong for a solver at a million per frame. That is Phase 7's step 4
(WGSL compute through the renderer), not a reason to grow this.
+**The Code row applies on ctrl+enter, Escape or leaving the row — never per
+keystroke.** cce-ui's `ParametersBg` code editor (line numbers, selection,
+clipboard, tab indenting, auto-indent, undo) keeps edits in its buffer
+until one of those, because this node evaluates on every value change and
+a half-typed line would fail on every keystroke — the border is amber
+while edits are pending. **A script error's line is flagged in the row**:
+`code_error_line_for_pane` in `render.rs` reads the `(line N` out of the
+evaluation error when the node it names is the one the pane shows, and
+hands it to `set_code_error_line`; Rhai's line numbers survive `desugar`
+because the `@` rewrite never adds or removes a line. Cleared on the next
+evaluation that says nothing about that node.
+
### GPU compute: the springs solve is the first operator (Phase 7 step 4)
`src/gpu.rs` keeps one `cce_ui::vk::ComputeDevice` per thread, opened on
diff --git a/shapeshifter.md b/shapeshifter.md
index 1e4c696..347c38c 100644
--- a/shapeshifter.md
+++ b/shapeshifter.md
@@ -600,9 +600,11 @@ to be undone to reach the fourth.
> Group, the `@name` sugar with typed attribute creation, `ch` / `chs` / `chv`
> / `chi` resolved through `TreeScope` before the run, topology and nearest,
> deferred `addpoint` / `addprim` / `removepoint`, both budgets. Nine tests
-> in `main.rs`. Not yet: a code editor worth the name in the params pane
-> (the `code` row is a single-line text box), `@N` write-back feeding the
-> normal overlay, and a vertex class.
+> in `main.rs`. The params pane's code row is an editor since the same
+> day: gutter, selection, clipboard, indenting, undo, apply on ctrl+enter
+> rather than per keystroke, and the failing line flagged from the
+> evaluation error. Not yet: `@N` write-back feeding the normal overlay,
+> and a vertex class.
**Step 1 — a `wrangle` node on an embedded engine.** Houdini's attribwrangle,
the thing users actually reach for, on a scripting engine someone else
diff --git a/src/application.rs b/src/application.rs
index 75bdd99..9a632bb 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -326,7 +326,9 @@ impl Application for State {
/// The toolkit's undo/redo routing lands here once no focused text box
/// wanted the chord. Only the curve viewer state has a history today.
fn undo(&mut self, needs_rebuild: &mut bool) -> bool {
- let taken = self.viewer_tool_undo();
+ // A code row being edited owns the chord: its typing is the thing to
+ // undo, ahead of a viewer tool that may also be active.
+ let taken = self.code_editor_action(cce_ui::widget::ContextAction::Undo) || self.viewer_tool_undo();
if taken {
*needs_rebuild = true;
}
@@ -334,7 +336,7 @@ impl Application for State {
}
fn redo(&mut self, needs_rebuild: &mut bool) -> bool {
- let taken = self.viewer_tool_redo();
+ let taken = self.code_editor_action(cce_ui::widget::ContextAction::Redo) || self.viewer_tool_redo();
if taken {
*needs_rebuild = true;
}
diff --git a/src/main.rs b/src/main.rs
index 289085b..e74f372 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10849,4 +10849,25 @@ mod tests {
);
}
}
+
+ /// A wrangle's error names its node and its line; the params pane is
+ /// told the line only when that node is the one it shows.
+ #[test]
+ fn a_script_error_line_reaches_the_params_pane_for_the_shown_node() {
+ use crate::app::State;
+ assert_eq!(State::error_line_number("syntax: Syntax error: Expecting ';' (line 3, position 5)"), Some(2));
+ assert_eq!(State::error_line_number("point 4: index out of range (line 1, position 9)"), Some(0));
+ assert_eq!(State::error_line_number("OpenCL nodes are retired"), None);
+ assert_eq!(State::error_line_number("(line 0, position 1)"), None, "a zero line is not a line");
+
+ let mut state = State::new(false);
+ let mut redraw = false;
+ state.apply_action(crate::app::McpAction::AddNode { template_name: "Wrangle".into(), name: Some("w".into()), x: 3.0, y: 9.0 }, &mut redraw).unwrap();
+ let slot = state.current_dir().children.iter().position(|c| c.name == "w").unwrap();
+ state.apply_action(crate::app::McpAction::Select { slot }, &mut redraw).unwrap();
+ assert_eq!(state.param_editor_selected(), Some(slot));
+ assert_eq!(state.code_error_line_for_pane("w: syntax: Syntax error (line 2, position 1)"), Some(1));
+ assert_eq!(state.code_error_line_for_pane("sphere1: something (line 2, position 1)"), None, "another node's error is not this pane's");
+ assert_eq!(state.code_error_line_for_pane("w: OpenCL nodes are retired"), None, "no line, no flag");
+ }
}
diff --git a/src/render.rs b/src/render.rs
index 688e29d..d6f07fd 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -1192,6 +1192,11 @@ impl State {
};
self.sim_cache = sim_cache;
+ // A script error names its line; if the node it names is the one the
+ // params pane shows, the pane flags that line in its code row. Cleared
+ // whenever the evaluation says nothing about that node.
+ let flagged = ocl_error.as_deref().and_then(|e| self.code_error_line_for_pane(e));
+ self.slots.param_bg_mut().set_code_error_line(flagged);
if let Some(e) = ocl_error {
self.update_status_text(&format!("Node error: {}", e));
} else {
@@ -1256,6 +1261,32 @@ impl State {
crate::geometry::rt_scene_from_verts(&verts)
}
+ /// The 0-based line a node error points at in the params pane's selected
+ /// node, when the error names that node and carries a `(line N` — the
+ /// shape Rhai's diagnostics take (`wrangle1: point 4: ... (line 3,
+ /// position 5)`). Anything else is None.
+ pub(crate) fn code_error_line_for_pane(&self, error: &str) -> Option<usize> {
+ let slot = self.param_editor_selected()?;
+ let node_name = &self.param_editor_dir().children.get(slot)?.name;
+ let rest = error.strip_prefix(node_name.as_str())?.strip_prefix(':')?;
+ Self::error_line_number(rest)
+ }
+
+ /// A clipboard, selection or history action for the params pane's code
+ /// editor, when one is open. False otherwise, so the caller's own
+ /// handling runs.
+ pub(crate) fn code_editor_action(&mut self, action: cce_ui::widget::ContextAction) -> bool {
+ let pane = self.slots.param_bg_mut();
+ pane.code_editing() && pane.code_action(action)
+ }
+
+ /// `(line N` anywhere in a diagnostic, 1-based in the text, 0-based out.
+ pub(crate) fn error_line_number(text: &str) -> Option<usize> {
+ let at = text.find("(line ")?;
+ let digits: String = text[at + 6..].chars().take_while(|c| c.is_ascii_digit()).collect();
+ digits.parse::<usize>().ok().filter(|n| *n >= 1).map(|n| n - 1)
+ }
+
pub(crate) fn update_status_text(&mut self, text: &str) {
if self.last_status_text != text {
self.last_status_text = text.to_string();
diff --git a/src/slots.rs b/src/slots.rs
index e76777e..2c2896b 100644
--- a/src/slots.rs
+++ b/src/slots.rs
@@ -185,6 +185,12 @@ impl WidgetSlots {
self.param.as_any_mut().downcast_mut::<ParametersBg>().expect("PARAM_IDX must be a ParametersBg")
}
+ /// The pane as its concrete type, for what `ParamController` does not
+ /// carry — the code row's error line.
+ pub fn param_bg_mut(&mut self) -> &mut ParametersBg {
+ self.param.as_any_mut().downcast_mut::<ParametersBg>().expect("PARAM_IDX must be a ParametersBg")
+ }
+
pub fn spreadsheet_mut(&mut self) -> &mut dyn cce_ui::widget::SpreadsheetController {
self.spreadsheet.as_any_mut().downcast_mut::<Spreadsheet>().expect("SPREADSHEET_IDX must be a Spreadsheet")
}