graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: an expression wire is drawn to the node it evaluates to
The Remesh subnet's transfer reads its From through an expression, which
the network drew as no line; it is drawn from input1 now, or from what
the Remesh's own From names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 8 +++++---
src/app.rs | 32 ++++++++++++++++++++++++--------
src/main.rs | 16 ++++++++++++++--
3 files changed, 43 insertions(+), 13 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 287b105..e09fa51 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1067,9 +1067,11 @@ way — both in `src/geometry.rs`:
rule: every `node` parameter, in order, the k-th into port k, handed to
the graph typed `node` (cce-ui's `wire_pairs`); a node gets as many input
ports as it has wires where its template declared fewer (Relax's Rest,
- Collision's Collider, the Remesh's From). A wire whose row is hidden, or
- that is an expression (the Remesh subnet's transfer reads its From
- through one), keeps its port and draws no line. A connection dropped on
+ Collision's Collider, the Remesh's From). A wire whose row is hidden
+ keeps its port and draws no line. An expression wire is drawn to what it
+ evaluates to at the current frame (`node_wires_at`) — the Remesh subnet's
+ transfer reads its From through `if(chs("../From"), …, "input1")` and is
+ drawn from input1 — and to nothing when that fails. A connection dropped on
port k sets the k-th wire (`State::connect_port`).
`every_wire_is_drawn_into_its_own_port` is the test.
diff --git a/src/app.rs b/src/app.rs
index 9512654..8965257 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -711,10 +711,24 @@ pub fn float3_row(min: f32, max: f32, trackball: bool) -> String {
/// A node's wires as the network draws them: every parameter of the `node`
/// kind, in order — the k-th is input port k — as (name, the node it names).
-/// A wire whose row is hidden (`show_when`) or that is an expression names
-/// nothing here: it keeps its port and draws no line, since what is not on
-/// screen should not be, and an expression is not a name until evaluated.
+/// A wire whose row is hidden (`show_when`) names nothing here: it keeps its
+/// port and draws no line, since what is not on screen should not be. An
+/// expression wire names what it EVALUATES to at `frame` — the Remesh
+/// subnet's transfer reads its From through `if(chs("../From"), …,
+/// "input1")`, and is drawn from input1 — and nothing when it fails.
/// Auto-layout reads the same wires.
+pub fn node_wires_at(root: &FsNode, node: &FsNode, frame: i32) -> Vec<(String, String)> {
+ if !node.params.iter().any(|p| p.kind() == ParamKind::Node && p.is_expr()) {
+ return node_wires(node);
+ }
+ match crate::geometry::resolve_param_refs(root, node, frame, &mut None) {
+ Some(resolved) => node_wires(&resolved),
+ None => node_wires(node),
+ }
+}
+
+/// [`node_wires_at`] with nothing evaluated: an expression wire names
+/// nothing.
pub fn node_wires(node: &FsNode) -> Vec<(String, String)> {
node.params
.iter()
@@ -6656,11 +6670,11 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
/// is what it is handed, and a node has as many input ports as it has
/// wires where its template declared fewer (Relax's Rest, Collision's
/// Collider, the Remesh's From).
- fn graph_nodes_of(dir: &FsNode) -> Vec<GraphNode> {
+ fn graph_nodes_of(root: &FsNode, dir: &FsNode, frame: i32) -> Vec<GraphNode> {
dir.children
.iter()
.map(|c| {
- let wires = node_wires(c);
+ let wires = node_wires_at(root, c, frame);
GraphNode {
id: c.id.clone(),
name: c.name.clone(),
@@ -6694,12 +6708,13 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
}
pub fn sync_nodes(&mut self) {
- let graph_nodes = Self::graph_nodes_of(self.current_dir());
+ let frame = self.sim_frame();
+ let graph_nodes = Self::graph_nodes_of(&self.fs_root, self.current_dir(), frame);
self.graph_mut().set_nodes(&graph_nodes);
// The second network editor views ITS OWN level.
self.clamp_path2();
- let nodes2 = Self::graph_nodes_of(self.dir_at(&self.current_path2.clone()));
+ let nodes2 = Self::graph_nodes_of(&self.fs_root, self.dir_at(&self.current_path2.clone()), frame);
use cce_ui::widget::GraphController as _;
self.slots.content2.set_nodes(&nodes2);
let names2 = self.path_names_at(&self.current_path2.clone());
@@ -8566,6 +8581,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
if self.focused_pane != LEFT_MENUBAR_IDX {
return false;
}
+ let frame = self.sim_frame();
let nodes: Vec<crate::layout::LayoutNode> = self
.current_dir()
.children
@@ -8578,7 +8594,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
.find(|p| p.name.eq_ignore_ascii_case("input"))
.map(|p| p.text().to_string()),
// The rest of the wires the network draws.
- reads: node_wires(c).into_iter().filter(|(name, _)| !name.eq_ignore_ascii_case("input")).map(|(_, src)| src).collect(),
+ reads: node_wires_at(&self.fs_root, c, frame).into_iter().filter(|(name, _)| !name.eq_ignore_ascii_case("input")).map(|(_, src)| src).collect(),
position: c.position,
// Utility trees stay where they were put; see the module doc.
pinned: false,
diff --git a/src/main.rs b/src/main.rs
index 7eaa9f2..7665510 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8114,7 +8114,9 @@ mod tests {
/// Every wire a node has is drawn, into its own port: the Remesh's
/// switch reads the loop on its Input and the transfer on its Input 2,
/// and both are lines on the network now, where only the Input was. An
- /// expression wire (the transfer's From) keeps its port and draws none.
+ /// expression wire (the transfer's From) is drawn to what it evaluates
+ /// to — input1 while the Remesh's own From is empty; a node beside the
+ /// subnet when it names one, which is not on this level to draw from.
/// A connection dropped on a port sets THAT wire, and auto-layout puts
/// a node below everything it reads.
#[test]
@@ -8133,7 +8135,17 @@ mod tests {
assert_eq!(node_wires(&switch), ["repeat1", "transfer1", "", ""]);
assert_eq!(switch.inputs, 4);
let transfer = get("transfer1");
- assert_eq!(node_wires(&transfer), ["repeat1", ""], "From is an expression: a port, no line");
+ assert_eq!(node_wires(&transfer), ["repeat1", "input1"], "From is an expression, drawn to what it evaluates to");
+
+ // The Remesh's From naming a node outside: not on this level, no line.
+ state.apply_action(McpAction::Up, &mut redraw).unwrap();
+ state.current_dir_mut().children[slot].params.iter_mut().find(|p| p.name == "From").unwrap().set_text("elsewhere");
+ state.apply_action(McpAction::Enter { slot }, &mut redraw).unwrap();
+ state.sync_nodes();
+ let level = state.graph().get_nodes();
+ let transfer = level.iter().find(|g| g.name == "transfer1").unwrap();
+ assert_eq!(node_wires(transfer), ["repeat1", "elsewhere"], "it names the outer node…");
+ assert!(!level.iter().any(|g| g.name == "elsewhere"), "…which is not on this level, so no line is drawn");
// Dropped on the switch's third port: Input 3.
let path = state.current_path.clone();