From d16435f2a9c97f98e7a737a3a7235fb539b28c94 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 29 Jun 2026 11:46:31 -0400 Subject: [PATCH] editor-core: expose the resolved layout for renderers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EditorSession kept only the RenderIR (its hit-test projection) and discarded the ResolvedLayoutIR the solver produced — but a renderer like epiphany-render-svg consumes the ResolvedLayoutIR, not the RenderIR. Keep it on the session and add resolved(), so a GUI can draw the score without re-running the solve. render_score now threads all three (resolved, render, map); they are updated together on open and on every committed edit. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS --- crates/epiphany-editor-core/src/lib.rs | 30 +++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/crates/epiphany-editor-core/src/lib.rs b/crates/epiphany-editor-core/src/lib.rs index 66b6de7..f2278eb 100644 --- a/crates/epiphany-editor-core/src/lib.rs +++ b/crates/epiphany-editor-core/src/lib.rs @@ -50,7 +50,7 @@ use epiphany_core::{ }; use epiphany_layout_ir::{ to_constrained, to_logical, to_render, ConstraintSolver, HitTestMap, LayoutObjectId, Point, - RenderIR, SolverConfig, + RenderIR, ResolvedLayoutIR, SolverConfig, }; use epiphany_ops::{ AcceptOutcome, AuthorId, CausalContext, DeleteEventOp, DeleteIdentifiedPitchOp, @@ -165,6 +165,10 @@ pub struct EditorSession { base: Score, score: Score, solver: Box, + // The solved layout the current render derives from — the input a renderer (e.g. + // epiphany-render-svg) consumes. Kept alongside the RenderIR, which is the + // hit-test projection of it. + resolved: ResolvedLayoutIR, render: RenderIR, map: HitTestMap, selection: Option, @@ -183,12 +187,13 @@ impl EditorSession { /// Opens a session on `score` with `solver`, rendering immediately. Errors with /// [`EditorError::NotRenderable`] if the initial layout is diagnostic-only. pub fn open(score: Score, solver: Box) -> Result { - let (render, map) = + let (resolved, render, map) = render_score(&score, solver.as_ref()).ok_or(EditorError::NotRenderable)?; Ok(EditorSession { base: score.clone(), score, solver, + resolved, render, map, selection: None, @@ -223,6 +228,13 @@ impl EditorSession { &self.score } + /// The current resolved layout — the input a renderer (e.g. `epiphany-render-svg`) + /// consumes to draw the score. The [`Self::render`] / [`Self::hit_test`] views are + /// its hit-test projection. + pub fn resolved(&self) -> &ResolvedLayoutIR { + &self.resolved + } + /// The current render, for the GUI to draw. pub fn render(&self) -> &RenderIR { &self.render @@ -366,12 +378,13 @@ impl EditorSession { let graph_changed = edited != self.score; // Refuse a diagnostic-only layout, still before committing anything. - let (render, map) = + let (resolved, render, map) = render_score(&edited, self.solver.as_ref()).ok_or(EditorError::NotRenderable)?; // Commit (the only mutation point — so an error above leaves all state, // op log included, untouched). self.score = edited; + self.resolved = resolved; self.render = render; self.map = map; self.applied.extend(new); @@ -1047,7 +1060,10 @@ fn staff_step_spelling(spelling: &PitchSpelling, steps: i32) -> Option Option<(RenderIR, HitTestMap)> { +fn render_score( + score: &Score, + solver: &dyn ConstraintSolver, +) -> Option<(ResolvedLayoutIR, RenderIR, HitTestMap)> { let report = solver.solve( &to_constrained(&to_logical(score)), &SolverConfig::default(), @@ -1057,7 +1073,7 @@ fn render_score(score: &Score, solver: &dyn ConstraintSolver) -> Option<(RenderI } let render = to_render(&report.layout); let map = render.hit_test_map(); - Some((render, map)) + Some((report.layout, render, map)) } #[cfg(test)] @@ -1128,6 +1144,10 @@ mod tests { let session = open_rich(0x5EED); assert!(!session.render().primitives.is_empty(), "the score renders"); assert!(!session.hit_test().regions.is_empty(), "with hit regions"); + assert!( + !session.resolved().glyphs.is_empty(), + "the resolved layout a renderer consumes is exposed" + ); assert_eq!(session.selection(), None, "nothing is selected at open"); }