Restore selected profile extrusion and preserve circle card aiming
Recognize explicitly selected closed sketch loops after Undo/Redo and Open using one connected walk shared by readiness and dispatch. Validate current structural references before changing history or drawing state, while leaving solved geometry validation in compilation. Preserve radius-to-Extrude aiming with action-specific parameter ownership. Add real profile, angle and solved point-measure workflows, retain the broad-gate regression evidence, and record the next natural-dimension repair requirements. Validation: 1451 workspace tests, 25 release Headless tests, 44 selftests; formatting and workspace Clippy pass. Nine release cross-process workflows pass, including unchanged Cut/Boss and Section goldens on llvmpipe. Five deliberately false assertions fail at their intended steps. Agent: codex-root
This commit is contained in:
@@ -416,6 +416,7 @@ mod tests {
|
||||
lines.curve_ids.push(point_key);
|
||||
}
|
||||
let rows = vec![vernier_ui::shell::SketchCurveRow {
|
||||
endpoints: None,
|
||||
radius_mm: Some(1.0),
|
||||
id: curve,
|
||||
sketch: id(3),
|
||||
|
||||
@@ -1206,41 +1206,12 @@ pub(crate) fn edit_for(action: Action, shell: &ShellState, view: &SceneView) ->
|
||||
})
|
||||
})
|
||||
}
|
||||
// A LONE CIRCLE IS A COMPLETE PROFILE — `validate_profile` says so,
|
||||
// and it is the whole reason this action is reachable for a circle
|
||||
// and not yet for a line: a polyline profile has to be walked and
|
||||
// proven closed, and a button offered before that exists would be a
|
||||
// button that cannot dispatch.
|
||||
Action::Extrude => {
|
||||
// A CLOSED CHAIN FIRST, matching `selection_of`'s own order: a
|
||||
// profile is why the button is on screen while sketching, and
|
||||
// the face pick behind it is stale. Falling back to a picked
|
||||
// circle keeps the two ways of naming a profile in one arm,
|
||||
// rather than two actions that mean the same thing.
|
||||
let chain = view
|
||||
.closed_profile
|
||||
.as_ref()
|
||||
.map(|profile| Edit::ExtrudeSketch {
|
||||
sketch: profile.sketch,
|
||||
profile: profile.curves.clone(),
|
||||
height: shell.extrude_height_mm,
|
||||
cut: shell.extrude_cut,
|
||||
});
|
||||
chain.or_else(|| {
|
||||
shell.picked_curves.first().copied().and_then(|curve| {
|
||||
view.sketch_curves
|
||||
.iter()
|
||||
.find(|row| {
|
||||
row.id == curve
|
||||
&& row.kind == vernier_ui::shell::SketchCurveKind::Circle
|
||||
})
|
||||
.map(|row| Edit::ExtrudeSketch {
|
||||
sketch: row.sketch,
|
||||
profile: vec![row.id],
|
||||
height: shell.extrude_height_mm,
|
||||
cut: shell.extrude_cut,
|
||||
})
|
||||
})
|
||||
vernier_ui::shell::profile_for_extrude(shell, view).map(|profile| Edit::ExtrudeSketch {
|
||||
sketch: profile.sketch,
|
||||
profile: profile.curves,
|
||||
height: shell.extrude_height_mm,
|
||||
cut: shell.extrude_cut,
|
||||
})
|
||||
}
|
||||
// THE SESSION'S COMMIT — either kind. Opening, arming and cancelling
|
||||
|
||||
@@ -217,6 +217,11 @@ const SOURCES: &[(&str, Role, &str)] = &[
|
||||
Role::Tests,
|
||||
include_str!("tests/sessions.rs"),
|
||||
),
|
||||
(
|
||||
"tests/existing_profiles.rs",
|
||||
Role::Tests,
|
||||
include_str!("tests/existing_profiles.rs"),
|
||||
),
|
||||
(
|
||||
"tests/parameter_cards.rs",
|
||||
Role::Tests,
|
||||
|
||||
@@ -201,14 +201,20 @@ pub(crate) fn apply(server: &mut DocumentServer, edit: &Edit) -> Result<bool, St
|
||||
height,
|
||||
cut,
|
||||
} => {
|
||||
// DRAWING ENDS WHEN THE SOLID BEGINS. The extrude consumes
|
||||
// the sketch, so leaving a chain or a centre pending would
|
||||
// let the next click extend geometry the body was already
|
||||
// built from.
|
||||
server.sketch_pending = None;
|
||||
server.pending_points.clear();
|
||||
server.sketch_chain.clear();
|
||||
server.sketch_active = None;
|
||||
let Some(feature) = server.document.features().get(sketch) else {
|
||||
return Err(stringy(vernier_doc::DocError::UnknownEntity(*sketch)));
|
||||
};
|
||||
let FeaturePayload::Sketch(data) = &feature.payload else {
|
||||
return Err(stringy(vernier_doc::DocError::UnknownEntity(*sketch)));
|
||||
};
|
||||
// Authored coordinates may be collapsed until constraints or
|
||||
// projections resolve. Admit identities/topology here; compile
|
||||
// retains full validation on the resolved, solved geometry.
|
||||
vernier_doc::validate_profile_structure(data, profile)
|
||||
.map_err(|error| error.to_string())?;
|
||||
if vernier_ui::shell::selected_profile(&server.sketch_curves(), profile).is_none() {
|
||||
return Err("select exactly one closed, unbranched profile".to_owned());
|
||||
}
|
||||
// A COUNTED DEFAULT (GAP 3) — `extrude`, then `extrude 2`. See
|
||||
// `DocumentServer::unique_name` for why the first keeps the
|
||||
// bare name and why this is a default rather than a rule.
|
||||
@@ -227,8 +233,16 @@ pub(crate) fn apply(server: &mut DocumentServer, edit: &Edit) -> Result<bool, St
|
||||
height: *height,
|
||||
target,
|
||||
})
|
||||
.map(|_| true)
|
||||
.map_err(stringy)
|
||||
.map_err(stringy)?;
|
||||
// DRAWING ENDS WHEN THE SOLID BEGINS. The extrude consumes
|
||||
// the sketch, so leaving a chain or a centre pending would
|
||||
// let the next click extend geometry the body was already
|
||||
// built from.
|
||||
server.sketch_pending = None;
|
||||
server.pending_points.clear();
|
||||
server.sketch_chain.clear();
|
||||
server.sketch_active = None;
|
||||
Ok(true)
|
||||
}
|
||||
Edit::CreateSweep {
|
||||
profile_sketch,
|
||||
|
||||
@@ -298,6 +298,7 @@ impl DocumentServer {
|
||||
// is what makes the drag's tie-break a property of the
|
||||
// geometry's vocabulary rather than of map iteration.
|
||||
let kind_points = kind.points();
|
||||
let endpoints = kind.endpoints();
|
||||
let geometry = self
|
||||
.solved_sketches
|
||||
.iter()
|
||||
@@ -347,6 +348,7 @@ impl DocumentServer {
|
||||
label
|
||||
};
|
||||
rows.push(vernier_ui::shell::SketchCurveRow {
|
||||
endpoints,
|
||||
radius_mm,
|
||||
id: curve,
|
||||
sketch: id,
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
//! Explicit selected-profile dispatch and stale worker admission.
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn existing_profile_stale_curve_refuses_before_history_or_drawing_changes() {
|
||||
let (mut server, block) = built_server();
|
||||
let sketch = *server.document.features().keys().next().unwrap();
|
||||
server.sketch_active = Some(sketch);
|
||||
server.sketch_pending = Some(block); // Explicit transient-state sentinel.
|
||||
server.sketch_chain = vec![block];
|
||||
server.pending_points = vec![block];
|
||||
server
|
||||
.document
|
||||
.execute(&vernier_doc::RenameFeature {
|
||||
id: block,
|
||||
name: "redo witness".into(),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(server.document.undo());
|
||||
let before = server.document.to_session_json().unwrap();
|
||||
let result = server.apply(&Edit::ExtrudeSketch {
|
||||
sketch,
|
||||
profile: vec![block],
|
||||
height: 2.0,
|
||||
cut: false,
|
||||
});
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"stale curve must refuse at admission: {result:?}"
|
||||
);
|
||||
assert_eq!(server.document.to_session_json().unwrap(), before);
|
||||
assert_eq!(server.sketch_active, Some(sketch));
|
||||
assert_eq!(server.sketch_pending, Some(block));
|
||||
assert_eq!(server.sketch_chain, vec![block]);
|
||||
assert_eq!(server.pending_points, vec![block]);
|
||||
assert!(server.document.redo());
|
||||
}
|
||||
|
||||
// Explicit document fixture/oracle construction, not claimed as GUI creation.
|
||||
fn fixture_loop(
|
||||
points: Vec<[f64; 2]>,
|
||||
curves: Vec<vernier_doc::CurveDraw>,
|
||||
constraints: Vec<vernier_doc::ConstraintDraw>,
|
||||
) -> (DocumentServer, DocEntityId) {
|
||||
let mut server = DocumentServer::empty();
|
||||
let sketch = server
|
||||
.document
|
||||
.execute(&AddSketchFeature {
|
||||
name: "selected loop".into(),
|
||||
plane: Default::default(),
|
||||
points: vec![],
|
||||
curves: vec![],
|
||||
constraints: vec![],
|
||||
})
|
||||
.unwrap()
|
||||
.created[0];
|
||||
server
|
||||
.document
|
||||
.execute(&vernier_doc::AddSketchGeometry {
|
||||
sketch,
|
||||
points,
|
||||
curves,
|
||||
constraints,
|
||||
construction: vec![],
|
||||
})
|
||||
.unwrap();
|
||||
(server, sketch)
|
||||
}
|
||||
|
||||
fn volume(server: &mut DocumentServer) -> f64 {
|
||||
compile_document(&mut server.document, &mut server.kernel, &mut server.store)
|
||||
.unwrap()
|
||||
.sole_body()
|
||||
.unwrap()
|
||||
.summary
|
||||
.geometry
|
||||
.volume
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_profile_arc_and_spline_dispatch_use_exact_ids_and_make_real_solids() {
|
||||
use vernier_doc::{CurveDraw as C, PointRef as P};
|
||||
for spline in [false, true] {
|
||||
let curved = if spline {
|
||||
C::Spline {
|
||||
points: vec![P::New(0), P::New(2), P::New(1)],
|
||||
}
|
||||
} else {
|
||||
C::Arc {
|
||||
center: P::New(3),
|
||||
start: P::New(0),
|
||||
end: P::New(1),
|
||||
}
|
||||
};
|
||||
let (mut server, sketch) = fixture_loop(
|
||||
vec![[5.0, 0.0], [-5.0, 0.0], [0.0, 5.0], [0.0, 0.0]],
|
||||
vec![
|
||||
curved,
|
||||
C::Line {
|
||||
start: P::New(1),
|
||||
end: P::New(0),
|
||||
},
|
||||
],
|
||||
vec![],
|
||||
);
|
||||
let scene = server.handle(Edit::Recompute);
|
||||
assert!(scene.error.is_none(), "{:?}", scene.error);
|
||||
assert!(scene.view.closed_profile.is_none());
|
||||
assert!(scene.view.active_sketch.is_none());
|
||||
let expected: Vec<_> = scene.view.sketch_curves.iter().map(|r| r.id).collect();
|
||||
for row in &scene.view.sketch_curves {
|
||||
let FeaturePayload::Sketch(data) = &server.document.features()[&sketch].payload else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(row.endpoints, data.curves[&row.id].endpoints());
|
||||
}
|
||||
let mut picks = expected.clone();
|
||||
picks.reverse();
|
||||
let shell = ShellState {
|
||||
picked_curves: picks,
|
||||
extrude_height_mm: 2.0,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
vernier_ui::shell::selection_of(&shell, &scene.view),
|
||||
Selection::Profile
|
||||
);
|
||||
let edit = edit_for(Action::Extrude, &shell, &scene.view).unwrap();
|
||||
assert!(matches!(&edit,Edit::ExtrudeSketch {profile,..} if *profile==expected));
|
||||
let done = server.handle(edit);
|
||||
assert!(done.error.is_none(), "{:?}", done.error);
|
||||
let measured = volume(&mut server);
|
||||
if spline {
|
||||
assert!(
|
||||
measured > 50.0 && measured < 150.0,
|
||||
"spline solid {measured}"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
(measured - 25.0 * std::f64::consts::PI).abs() < 1e-8,
|
||||
"semicircle area ×2 {measured}"
|
||||
);
|
||||
}
|
||||
assert!(server.handle(Edit::UndoRedo { back: true }).error.is_none());
|
||||
assert!(
|
||||
server
|
||||
.handle(Edit::UndoRedo { back: false })
|
||||
.error
|
||||
.is_none()
|
||||
);
|
||||
assert!((volume(&mut server) - measured).abs() < 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_profile_worker_admits_solver_expanded_authored_collapsed_rectangle() {
|
||||
use vernier_doc::{ConstraintDraw as D, CurveDraw as C, PointRef as P};
|
||||
let targets = [[0.0, 0.0], [20.0, 0.0], [20.0, 10.0], [0.0, 10.0]];
|
||||
let (mut server, sketch) = fixture_loop(
|
||||
vec![[0.0, 0.0]; 4],
|
||||
(0..4)
|
||||
.map(|i| C::Line {
|
||||
start: P::New(i),
|
||||
end: P::New((i + 1) % 4),
|
||||
})
|
||||
.collect(),
|
||||
targets
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &[x, y])| D::Lock {
|
||||
point: P::New(i),
|
||||
x,
|
||||
y,
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
let scene = server.handle(Edit::Recompute);
|
||||
assert!(scene.error.is_none(), "{:?}", scene.error);
|
||||
let shell = ShellState {
|
||||
picked_curves: scene
|
||||
.view
|
||||
.sketch_curves
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|r| r.id)
|
||||
.collect(),
|
||||
extrude_height_mm: 2.0,
|
||||
..Default::default()
|
||||
};
|
||||
let edit = edit_for(Action::Extrude, &shell, &scene.view).unwrap();
|
||||
let Edit::ExtrudeSketch { profile, .. } = &edit else {
|
||||
panic!()
|
||||
};
|
||||
let FeaturePayload::Sketch(data) = &server.document.features()[&sketch].payload else {
|
||||
panic!()
|
||||
};
|
||||
assert!(matches!(
|
||||
vernier_doc::validate_profile(data, profile),
|
||||
Err(vernier_doc::ProfileError::DegenerateCurve(_))
|
||||
));
|
||||
assert!(server.handle(edit).error.is_none());
|
||||
let measured = volume(&mut server);
|
||||
let solved = &server
|
||||
.solved_sketches
|
||||
.iter()
|
||||
.find(|(id, _)| *id == sketch)
|
||||
.unwrap()
|
||||
.1;
|
||||
let vertices: Vec<_> = solved.points.values().copied().collect();
|
||||
let twice_area: f64 = (0..4)
|
||||
.map(|i| {
|
||||
let a = vertices[i];
|
||||
let b = vertices[(i + 1) % 4];
|
||||
a[0] * b[1] - a[1] * b[0]
|
||||
})
|
||||
.sum();
|
||||
// At height2, the shoelace sum is the independent solved-volume oracle.
|
||||
assert!((measured - twice_area.abs()).abs() < 1e-8);
|
||||
// The solver's coordinate residual remains bounded against authored intent.
|
||||
assert!(
|
||||
(measured / 400.0 - 1.0).abs() < 1e-9,
|
||||
"solved rectangle volume {measured:.15}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_profile_preflight_does_not_solve_unresolved_projected_coordinates() {
|
||||
use vernier_doc::{CurveDraw as C, PointRef as P};
|
||||
let (mut server, _) = built_server();
|
||||
let scene = server.handle(Edit::Recompute);
|
||||
let face = |axis: usize| {
|
||||
scene
|
||||
.faces
|
||||
.iter()
|
||||
.find(|f| f.normal.is_some_and(|n| n[axis] > 0.9))
|
||||
.unwrap()
|
||||
.raw
|
||||
};
|
||||
let faces = [face(0), face(1), face(2)];
|
||||
assert!(
|
||||
server
|
||||
.handle(Edit::NewSketch {
|
||||
plane: crate::edit::NewSketchPlane::Face(faces[2])
|
||||
})
|
||||
.error
|
||||
.is_none()
|
||||
);
|
||||
let sketch = server.sketch_active.unwrap();
|
||||
let points: Vec<_> = faces
|
||||
.iter()
|
||||
.map(|&face| {
|
||||
server
|
||||
.document
|
||||
.execute(&vernier_doc::ProjectFaceIntoSketch {
|
||||
feature: sketch,
|
||||
face,
|
||||
})
|
||||
.unwrap()
|
||||
.created[0]
|
||||
})
|
||||
.collect();
|
||||
server
|
||||
.document
|
||||
.execute(&vernier_doc::AddSketchGeometry {
|
||||
sketch,
|
||||
points: vec![],
|
||||
curves: (0..3)
|
||||
.map(|i| C::Line {
|
||||
start: P::Existing(points[i]),
|
||||
end: P::Existing(points[(i + 1) % 3]),
|
||||
})
|
||||
.collect(),
|
||||
constraints: vec![],
|
||||
construction: vec![],
|
||||
})
|
||||
.unwrap();
|
||||
let scene = server.handle(Edit::FinishSketch);
|
||||
assert!(scene.error.is_none(), "{:?}", scene.error);
|
||||
let shell = ShellState {
|
||||
picked_curves: scene
|
||||
.view
|
||||
.sketch_curves
|
||||
.iter()
|
||||
.filter(|r| r.sketch == sketch)
|
||||
.rev()
|
||||
.map(|r| r.id)
|
||||
.collect(),
|
||||
extrude_height_mm: 2.0,
|
||||
..Default::default()
|
||||
};
|
||||
let edit = edit_for(Action::Extrude, &shell, &scene.view).unwrap();
|
||||
let Edit::ExtrudeSketch { profile, .. } = &edit else {
|
||||
panic!()
|
||||
};
|
||||
let FeaturePayload::Sketch(data) = &server.document.features()[&sketch].payload else {
|
||||
panic!()
|
||||
};
|
||||
assert!(points.iter().all(|p| data.points[p] == [0.0, 0.0]));
|
||||
assert!(matches!(
|
||||
vernier_doc::validate_profile(data, profile),
|
||||
Err(vernier_doc::ProfileError::DegenerateCurve(_))
|
||||
));
|
||||
assert!(
|
||||
server
|
||||
.solved_sketches
|
||||
.iter()
|
||||
.find(|(id, _)| *id == sketch)
|
||||
.unwrap()
|
||||
.1
|
||||
.points
|
||||
.values()
|
||||
.any(|p| p[0].abs() > 1.0 || p[1].abs() > 1.0)
|
||||
);
|
||||
let done = server.handle(edit);
|
||||
assert!(done.error.is_none(), "{:?}", done.error);
|
||||
assert!((volume(&mut server) - 18_300.0).abs() < 1e-7);
|
||||
}
|
||||
|
||||
struct ReplaceSketch {
|
||||
sketch: DocEntityId,
|
||||
data: vernier_doc::SketchData,
|
||||
}
|
||||
impl vernier_doc::Command for ReplaceSketch {
|
||||
fn apply(
|
||||
&self,
|
||||
ctx: &mut vernier_doc::CommandContext<'_>,
|
||||
) -> Result<vernier_doc::CommandOutput, vernier_doc::DocError> {
|
||||
ctx.feature_mut(self.sketch)?.payload = FeaturePayload::Sketch(self.data.clone());
|
||||
Ok(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_profile_stale_worker_snapshot_preserves_redo_allocator_and_gesture() {
|
||||
for damage in [
|
||||
"missing_point",
|
||||
"construction",
|
||||
"changed_endpoint",
|
||||
"branch",
|
||||
"wrong_sketch",
|
||||
] {
|
||||
let (mut server, block) = built_server();
|
||||
let scene = server.handle(Edit::Recompute);
|
||||
let sketch = *server.document.features().keys().next().unwrap();
|
||||
let shell = ShellState {
|
||||
picked_curves: scene
|
||||
.view
|
||||
.sketch_curves
|
||||
.iter()
|
||||
.filter(|r| r.sketch == sketch)
|
||||
.rev()
|
||||
.map(|r| r.id)
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut edit = edit_for(Action::Extrude, &shell, &scene.view).unwrap();
|
||||
let FeaturePayload::Sketch(mut data) = server.document.features()[&sketch].payload.clone()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
let curves: Vec<_> = data.curves.keys().copied().collect();
|
||||
let points: Vec<_> = data.points.keys().copied().collect();
|
||||
match damage {
|
||||
"missing_point" => {
|
||||
data.points.shift_remove(&points[0]);
|
||||
}
|
||||
"construction" => {
|
||||
data.construction.insert(curves[0]);
|
||||
}
|
||||
"changed_endpoint" => {
|
||||
data.curves[&curves[0]] = vernier_doc::SketchCurve::Line {
|
||||
start: points[0],
|
||||
end: points[2],
|
||||
};
|
||||
}
|
||||
"branch" => {
|
||||
for (id, (a, b)) in curves.iter().zip([(0, 1), (1, 2), (2, 1), (1, 0)]) {
|
||||
data.curves[id] = vernier_doc::SketchCurve::Line {
|
||||
start: points[a],
|
||||
end: points[b],
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Edit::ExtrudeSketch { sketch, .. } = &mut edit {
|
||||
*sketch = block;
|
||||
}
|
||||
}
|
||||
}
|
||||
server
|
||||
.document
|
||||
.execute(&ReplaceSketch { sketch, data })
|
||||
.unwrap();
|
||||
server
|
||||
.document
|
||||
.execute(&vernier_doc::RenameFeature {
|
||||
id: block,
|
||||
name: "redo witness".into(),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(server.document.undo());
|
||||
server.sketch_active = Some(sketch);
|
||||
server.sketch_pending = Some(points[0]);
|
||||
server.sketch_chain = curves.clone();
|
||||
server.pending_points = points.clone();
|
||||
let before = server.document.to_session_json().unwrap();
|
||||
let dirty = server.unsaved;
|
||||
let result = server.apply(&edit);
|
||||
assert!(result.is_err(), "{damage}: {result:?}");
|
||||
assert_eq!(
|
||||
server.document.to_session_json().unwrap(),
|
||||
before,
|
||||
"{damage}"
|
||||
);
|
||||
assert_eq!(server.unsaved, dirty);
|
||||
assert_eq!(server.sketch_active, Some(sketch));
|
||||
assert_eq!(server.sketch_pending, Some(points[0]));
|
||||
assert_eq!(server.sketch_chain, curves);
|
||||
assert_eq!(server.pending_points, points);
|
||||
assert!(server.document.redo());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_profile_invalid_explicit_pick_cannot_dispatch_another_transient_profile() {
|
||||
let (mut server, _) = built_server();
|
||||
let mut view = server.handle(Edit::Recompute).view;
|
||||
let sketch = *server.document.features().keys().next().unwrap();
|
||||
let curves: Vec<_> = view
|
||||
.sketch_curves
|
||||
.iter()
|
||||
.filter(|r| r.sketch == sketch)
|
||||
.map(|r| r.id)
|
||||
.collect();
|
||||
view.closed_profile = Some(vernier_ui::shell::ClosedProfile {
|
||||
sketch,
|
||||
curves: curves.clone(),
|
||||
});
|
||||
let shell = ShellState {
|
||||
picked_curves: curves[..3].to_vec(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(edit_for(Action::Extrude, &shell, &view).is_none());
|
||||
assert!(
|
||||
!actions_for(vernier_ui::shell::selection_of(&shell, &view)).contains(&Action::Extrude)
|
||||
);
|
||||
let shell = ShellState::default();
|
||||
assert!(edit_for(Action::Extrude, &shell, &view).is_some());
|
||||
}
|
||||
@@ -35,6 +35,7 @@ use winit::event::MouseButton;
|
||||
mod constraints;
|
||||
mod document;
|
||||
mod drawing;
|
||||
mod existing_profiles;
|
||||
mod features;
|
||||
mod file_only_save;
|
||||
mod frame;
|
||||
|
||||
@@ -979,6 +979,7 @@ fn offsetting_a_picked_square_grows_it_exactly() {
|
||||
sketch_curves: chain
|
||||
.iter()
|
||||
.map(|&id| vernier_ui::shell::SketchCurveRow {
|
||||
endpoints: None,
|
||||
radius_mm: Some(1.0),
|
||||
id,
|
||||
sketch,
|
||||
@@ -1071,6 +1072,7 @@ fn mirroring_a_picked_line_about_a_picked_axis_reflects_it_exactly() {
|
||||
let view = SceneView {
|
||||
sketch_curves: vec![
|
||||
vernier_ui::shell::SketchCurveRow {
|
||||
endpoints: None,
|
||||
radius_mm: Some(1.0),
|
||||
id: line,
|
||||
sketch,
|
||||
@@ -1080,6 +1082,7 @@ fn mirroring_a_picked_line_about_a_picked_axis_reflects_it_exactly() {
|
||||
defining_points: Vec::new(),
|
||||
},
|
||||
vernier_ui::shell::SketchCurveRow {
|
||||
endpoints: None,
|
||||
radius_mm: Some(1.0),
|
||||
id: axis,
|
||||
sketch,
|
||||
|
||||
@@ -59,8 +59,8 @@ pub use sketch::{
|
||||
SketchData, SketchDiagnostics, SketchError, SketchFrame, SketchPlane, SketchReference,
|
||||
SketchState, SweepError, infer_constraints_for_curve, infer_provisional, is_full_turn,
|
||||
mirror_plane_geometry, nearest_defining_point, nearest_of, solve_sketch, validate_loft,
|
||||
validate_mirror_plane, validate_path, validate_profile, validate_revolve, validate_sweep,
|
||||
validate_sweep_guide,
|
||||
validate_mirror_plane, validate_path, validate_profile, validate_profile_structure,
|
||||
validate_revolve, validate_sweep, validate_sweep_guide,
|
||||
};
|
||||
// Appended by M2 lane C2 (edge/vertex references), for the same reason the
|
||||
// M3 block above is its own block: a merge that appends is a merge with no
|
||||
|
||||
@@ -833,7 +833,33 @@ pub fn validate_profile(
|
||||
check_curve_geometry(data, id, curve.clone())?;
|
||||
}
|
||||
|
||||
if let ([only], [curve]) = (chain, curves.as_slice()) {
|
||||
profile_walk(chain, &curves)
|
||||
}
|
||||
|
||||
/// Validates an ordered profile's structure without interpreting authored
|
||||
/// coordinates as solved geometry. Suitable for admission before history or
|
||||
/// allocator mutation. The compiler must still call [`validate_profile`] after
|
||||
/// resolving projections and solving constraints.
|
||||
///
|
||||
/// Checks construction, live curves and defining points, repetition, connected
|
||||
/// traversal and closure. It does not detect geometric collapse/intersections.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns the existing [`ProfileError`] for the first structural failure.
|
||||
pub fn validate_profile_structure(
|
||||
data: &SketchData,
|
||||
chain: &[EntityId],
|
||||
) -> Result<Vec<ProfileLink>, ProfileError> {
|
||||
reject_construction(data, chain)?;
|
||||
let curves = chain_curves(data, chain)?;
|
||||
profile_walk(chain, &curves)
|
||||
}
|
||||
|
||||
fn profile_walk(
|
||||
chain: &[EntityId],
|
||||
curves: &[SketchCurve],
|
||||
) -> Result<Vec<ProfileLink>, ProfileError> {
|
||||
if let ([only], [curve]) = (chain, curves) {
|
||||
let only = *only;
|
||||
return match *curve {
|
||||
SketchCurve::Circle { .. } => Ok(vec![ProfileLink {
|
||||
@@ -848,7 +874,7 @@ pub fn validate_profile(
|
||||
};
|
||||
}
|
||||
|
||||
let walk = walk_chain(chain, &curves)?;
|
||||
let walk = walk_chain(chain, curves)?;
|
||||
if walk.last != walk.first {
|
||||
return Err(ProfileError::NotClosed);
|
||||
}
|
||||
@@ -4472,6 +4498,52 @@ mod tests {
|
||||
(f, chain)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_structure_leaves_geometry_to_the_solved_validator_and_preserves_error_order() {
|
||||
let (mut f, chain) = rectangle_fixture();
|
||||
let (a, b) = f.data.curves[&chain[0]].endpoints().unwrap();
|
||||
f.data.points[&b] = f.data.points[&a];
|
||||
assert!(validate_profile_structure(&f.data, &chain).is_ok());
|
||||
assert_eq!(
|
||||
validate_profile(&f.data, &chain),
|
||||
Err(ProfileError::DegenerateCurve(chain[0]))
|
||||
);
|
||||
let disconnected = [chain[0], chain[2], chain[1], chain[3]];
|
||||
assert_eq!(
|
||||
validate_profile_structure(&f.data, &disconnected),
|
||||
Err(ProfileError::NotConnected(chain[2]))
|
||||
);
|
||||
// Full validation historically reports geometry before topology.
|
||||
assert_eq!(
|
||||
validate_profile(&f.data, &disconnected),
|
||||
Err(ProfileError::DegenerateCurve(chain[0]))
|
||||
);
|
||||
f.data.points.shift_remove(&b);
|
||||
assert_eq!(
|
||||
validate_profile_structure(&f.data, &chain),
|
||||
Err(ProfileError::UnknownPoint(b))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_structure_accepts_authored_collapsed_but_solver_expanded_geometry() {
|
||||
let (mut f, chain) = rectangle_fixture();
|
||||
let targets: Vec<_> = f.data.points.iter().map(|(&id, &at)| (id, at)).collect();
|
||||
for (point, [x, y]) in targets {
|
||||
f.data
|
||||
.constraints
|
||||
.push(SketchConstraint::Lock { point, x, y });
|
||||
f.data.points[&point] = [0.0, 0.0];
|
||||
}
|
||||
assert!(validate_profile_structure(&f.data, &chain).is_ok());
|
||||
assert!(matches!(
|
||||
validate_profile(&f.data, &chain),
|
||||
Err(ProfileError::DegenerateCurve(_))
|
||||
));
|
||||
solve_sketch(&mut f.data).unwrap();
|
||||
assert!(validate_profile(&f.data, &chain).is_ok());
|
||||
}
|
||||
|
||||
/// The happy path, both windings. A profile authored clockwise and one
|
||||
/// authored counter-clockwise must resolve to the same curves with
|
||||
/// consistent traversal — the orientation of the face is the kernel's
|
||||
|
||||
@@ -25,6 +25,7 @@ pub fn selection_name(selection: Selection) -> String {
|
||||
}
|
||||
Selection::Feature(kind) => format!("feature:{}", feature_kind_name(kind)),
|
||||
Selection::Circle => "circle".to_owned(),
|
||||
Selection::ConstructionCircle => "construction-circle".to_owned(),
|
||||
Selection::Line => "line".to_owned(),
|
||||
Selection::Point { locked, .. } => {
|
||||
format!("point:{}", if locked { "locked" } else { "free" })
|
||||
|
||||
@@ -48,6 +48,8 @@ fn options() -> Options {
|
||||
fn modeling_workflows_produce_their_exported_geometry() {
|
||||
for name in [
|
||||
"circle-closed-profile",
|
||||
"existing-profile-redo",
|
||||
"existing-profile-open",
|
||||
"small-profile-40",
|
||||
"small-profile-80",
|
||||
"snap-alt-override",
|
||||
@@ -69,6 +71,7 @@ fn modeling_workflows_produce_their_exported_geometry() {
|
||||
"functions-mirror",
|
||||
"functions-arc",
|
||||
"functions-polygon",
|
||||
"functions-sketch-angle",
|
||||
"parameter-cards-exact",
|
||||
"save-preserves-selection",
|
||||
"save-preserves-sketch",
|
||||
@@ -86,6 +89,34 @@ fn modeling_workflows_produce_their_exported_geometry() {
|
||||
}
|
||||
}
|
||||
|
||||
/// An open sketch has no solid to export. Actual point measurement must read
|
||||
/// solved geometry, while native constraints and real Undo/Redo pin its edits.
|
||||
#[test]
|
||||
fn point_distance_controls_measure_solved_geometry_and_preserve_constraints() {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../scripts/drive/functions-point-distance-measure.json");
|
||||
let report = drive(
|
||||
&path,
|
||||
&out_dir("functions-point-distance-measure"),
|
||||
Determinism::Once,
|
||||
options(),
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("point-distance-measure: {error}"));
|
||||
assert_eq!(report.result, "pass");
|
||||
assert!(
|
||||
report
|
||||
.trace
|
||||
.iter()
|
||||
.any(|step| step.step == "expect_document")
|
||||
);
|
||||
assert!(
|
||||
report
|
||||
.trace
|
||||
.iter()
|
||||
.any(|step| step.step == "expect_readout")
|
||||
);
|
||||
}
|
||||
|
||||
/// Trim changes an open sketch; these scripts assert its native topology and
|
||||
/// real Undo. The headless companion supplies the restored-profile STEP oracle.
|
||||
#[test]
|
||||
|
||||
@@ -23,6 +23,7 @@ mod gizmo_overlay;
|
||||
mod parameter_card;
|
||||
mod proposal_overlay;
|
||||
mod ribbon;
|
||||
mod selected_profile;
|
||||
mod settings_screen;
|
||||
mod sketch_mode;
|
||||
mod timeline;
|
||||
@@ -31,6 +32,7 @@ pub use parameter_card::{
|
||||
ParameterSubmission, acknowledge_parameter_card, card_canonical_value, card_submission,
|
||||
reconcile_parameter_card,
|
||||
};
|
||||
pub use selected_profile::{profile_for_extrude, selected_profile};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -1347,6 +1349,8 @@ pub struct ClosedProfile {
|
||||
/// One selectable curve of one sketch.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SketchCurveRow {
|
||||
/// Exact endpoint identities from `SketchCurve::endpoints`; circles have none.
|
||||
pub endpoints: Option<(DocEntityId, DocEntityId)>,
|
||||
/// Exact editable radius target, or unconstrained geometric radius.
|
||||
pub radius_mm: Option<f64>,
|
||||
/// The curve.
|
||||
@@ -2044,6 +2048,14 @@ pub fn selection_of(state: &ShellState, view: &SceneView) -> Selection {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// An explicit closed set is a profile even after Undo/Open cleared the
|
||||
// drawing gesture. Keep a lone circle's radius-card classification.
|
||||
if state.picked_curves.len() > 1 && profile_for_extrude(state, view).is_some() {
|
||||
return Selection::Profile;
|
||||
}
|
||||
if state.picked_curves.len() > 2 {
|
||||
return Selection::None;
|
||||
}
|
||||
// TWO LINES FIRST: the two-entity constraints are what a second pick is
|
||||
// FOR, so a pair must not fall through to the single-curve arm and
|
||||
// silently offer Horizontal for whichever one was picked last.
|
||||
@@ -2074,6 +2086,15 @@ pub fn selection_of(state: &ShellState, view: &SceneView) -> Selection {
|
||||
};
|
||||
}
|
||||
if let Some(curve) = state.picked_curves.first().copied() {
|
||||
if view
|
||||
.sketch_curves
|
||||
.iter()
|
||||
.filter(|row| row.id == curve)
|
||||
.count()
|
||||
!= 1
|
||||
{
|
||||
return Selection::None;
|
||||
}
|
||||
return view
|
||||
.sketch_curves
|
||||
.iter()
|
||||
@@ -2083,6 +2104,7 @@ pub fn selection_of(state: &ShellState, view: &SceneView) -> Selection {
|
||||
// `Circle`: offering a radius edit for a curve that is gone is
|
||||
// how a dimension lands on whatever now holds that id.
|
||||
.map_or(Selection::None, |row| match row.kind {
|
||||
SketchCurveKind::Circle if row.construction => Selection::ConstructionCircle,
|
||||
SketchCurveKind::Circle => Selection::Circle,
|
||||
SketchCurveKind::Line => Selection::Line,
|
||||
// AN ARC NOW HAS ONE OF ITS OWN (M3-7a): its radius. The
|
||||
|
||||
@@ -13,6 +13,7 @@ struct Subject {
|
||||
edges: Vec<vernier_doc::EdgeRef>,
|
||||
vertices: Vec<vernier_doc::VertexRef>,
|
||||
curves: Vec<(DocEntityId, DocEntityId, SketchCurveKind)>,
|
||||
profile_curves: Vec<ProfileCurve>,
|
||||
points: Vec<(DocEntityId, DocEntityId)>,
|
||||
dimension: Option<(DocEntityId, usize, vernier_doc::SketchConstraint)>,
|
||||
profile: Option<ClosedProfile>,
|
||||
@@ -20,6 +21,13 @@ struct Subject {
|
||||
session: Option<OpenSession>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct ProfileCurve {
|
||||
id: DocEntityId,
|
||||
endpoints: Option<(DocEntityId, DocEntityId)>,
|
||||
construction: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
struct Chips {
|
||||
chamfer: bool,
|
||||
@@ -116,6 +124,20 @@ pub(super) fn dimension_identity(
|
||||
}
|
||||
|
||||
fn subject(state: &ShellState, view: &SceneView, action: Action) -> Subject {
|
||||
let profile = if action == Action::Extrude {
|
||||
profile_for_extrude(state, view)
|
||||
} else {
|
||||
view.closed_profile.clone()
|
||||
};
|
||||
let curves = if action == Action::Extrude {
|
||||
profile
|
||||
.as_ref()
|
||||
.map_or(state.picked_curves.as_slice(), |profile| {
|
||||
profile.curves.as_slice()
|
||||
})
|
||||
} else {
|
||||
state.picked_curves.as_slice()
|
||||
};
|
||||
Subject {
|
||||
action,
|
||||
feature: state.selected_feature,
|
||||
@@ -123,8 +145,7 @@ fn subject(state: &ShellState, view: &SceneView, action: Action) -> Subject {
|
||||
faces: state.picked_faces.clone(),
|
||||
edges: state.picked_edges.clone(),
|
||||
vertices: state.picked_vertices.clone(),
|
||||
curves: state
|
||||
.picked_curves
|
||||
curves: curves
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
view.sketch_curves
|
||||
@@ -133,6 +154,19 @@ fn subject(state: &ShellState, view: &SceneView, action: Action) -> Subject {
|
||||
.map(|row| (row.id, row.sketch, row.kind))
|
||||
})
|
||||
.collect(),
|
||||
profile_curves: if action == Action::Extrude {
|
||||
curves
|
||||
.iter()
|
||||
.filter_map(|id| view.sketch_curves.iter().find(|row| row.id == *id))
|
||||
.map(|row| ProfileCurve {
|
||||
id: row.id,
|
||||
endpoints: row.endpoints,
|
||||
construction: row.construction,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
points: state
|
||||
.picked_points
|
||||
.iter()
|
||||
@@ -146,7 +180,7 @@ fn subject(state: &ShellState, view: &SceneView, action: Action) -> Subject {
|
||||
dimension: state.picked_dimension.and_then(|picked| {
|
||||
dimension_identity(picked.expect).map(|identity| (picked.sketch, picked.at, identity))
|
||||
}),
|
||||
profile: view.closed_profile.clone(),
|
||||
profile,
|
||||
active_sketch: view.active_sketch,
|
||||
session: state.session.clone(),
|
||||
}
|
||||
@@ -344,8 +378,10 @@ pub fn reconcile_parameter_card(state: &mut ShellState, view: &SceneView) {
|
||||
if let Some(draft) = &state.card.draft
|
||||
&& draft.subject != owner
|
||||
{
|
||||
let mut same_selection = owner.clone();
|
||||
same_selection.action = draft.subject.action;
|
||||
// Ownership contains action-specific projections (Extrude's ordered
|
||||
// profile, for example). Compare the current selection through the
|
||||
// previous action's rules, not by relabelling the new action's owner.
|
||||
let same_selection = subject(state, view, draft.subject.action);
|
||||
let requested_arm = (same_selection == draft.subject)
|
||||
.then_some(state.card.armed)
|
||||
.flatten();
|
||||
@@ -753,6 +789,130 @@ mod tests {
|
||||
(state, view)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parameter_circle_radius_can_aim_extrude_without_losing_its_subject() {
|
||||
// Radius and Extrude intentionally project different ownership data
|
||||
// from the same circle. A command switch is not a selection change.
|
||||
for transient in [false, true] {
|
||||
let mut view = SceneView {
|
||||
sketch_curves: vec![SketchCurveRow {
|
||||
id: test_id(2),
|
||||
sketch: test_id(1),
|
||||
kind: SketchCurveKind::Circle,
|
||||
label: "circle".into(),
|
||||
endpoints: None,
|
||||
construction: false,
|
||||
radius_mm: Some(4.123456789),
|
||||
defining_points: vec![],
|
||||
}],
|
||||
closed_profile: transient.then_some(ClosedProfile {
|
||||
sketch: test_id(1),
|
||||
curves: vec![test_id(2)],
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let mut state = ShellState {
|
||||
picked_curves: vec![test_id(2)],
|
||||
..Default::default()
|
||||
};
|
||||
reconcile_parameter_card(&mut state, &view);
|
||||
assert_eq!(state.card.action, Some(Action::EditDimension));
|
||||
state.card.arm(Action::Extrude);
|
||||
reconcile_parameter_card(&mut state, &view);
|
||||
assert_eq!(state.card.armed, Some(Action::Extrude));
|
||||
assert_eq!(state.card.action, Some(Action::Extrude));
|
||||
state.card.buffers[0] = "7.".into();
|
||||
field_changed(&mut state, 0);
|
||||
state.extrude_cut = true;
|
||||
view.sketch_curves[0].radius_mm = Some(5.123456789);
|
||||
reconcile_parameter_card(&mut state, &view);
|
||||
assert_eq!(state.card.buffers, ["7."]);
|
||||
assert!(state.extrude_cut);
|
||||
state.card.arm(Action::EditDimension);
|
||||
reconcile_parameter_card(&mut state, &view);
|
||||
assert_eq!(state.card.armed, Some(Action::EditDimension));
|
||||
assert_eq!(state.card.action, Some(Action::EditDimension));
|
||||
assert_eq!(state.radius_mm.to_bits(), 5.123456789_f64.to_bits());
|
||||
|
||||
// A different picked circle really does retire the old aim.
|
||||
view.sketch_curves[0].id = test_id(3);
|
||||
state.picked_curves = vec![test_id(3)];
|
||||
state.card.arm(Action::Extrude);
|
||||
reconcile_parameter_card(&mut state, &view);
|
||||
assert_eq!(state.card.armed, None);
|
||||
assert_eq!(state.card.action, Some(Action::EditDimension));
|
||||
|
||||
state.card.arm(Action::Extrude);
|
||||
reconcile_parameter_card(&mut state, &view);
|
||||
assert_eq!(state.card.action, Some(Action::Extrude));
|
||||
state.card.buffers[0] = "9.".into();
|
||||
field_changed(&mut state, 0);
|
||||
view.sketch_curves[0].construction = true;
|
||||
reconcile_parameter_card(&mut state, &view);
|
||||
assert_eq!(state.card.armed, None);
|
||||
assert_eq!(state.card.action, Some(Action::EditDimension));
|
||||
assert!(!state.card.buffers.contains(&"9.".into()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parameter_selected_profile_subject_is_canonical_and_retires_only_structural_changes() {
|
||||
let mut view = SceneView {
|
||||
sketch_curves: vec![
|
||||
SketchCurveRow {
|
||||
id: test_id(6),
|
||||
sketch: test_id(1),
|
||||
kind: SketchCurveKind::Arc,
|
||||
label: "arc".into(),
|
||||
endpoints: Some((test_id(2), test_id(3))),
|
||||
construction: false,
|
||||
radius_mm: Some(5.0),
|
||||
defining_points: vec![],
|
||||
},
|
||||
SketchCurveRow {
|
||||
id: test_id(7),
|
||||
sketch: test_id(1),
|
||||
kind: SketchCurveKind::Line,
|
||||
label: "line".into(),
|
||||
endpoints: Some((test_id(3), test_id(2))),
|
||||
construction: false,
|
||||
radius_mm: None,
|
||||
defining_points: vec![],
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
let mut state = ShellState {
|
||||
picked_curves: vec![test_id(7), test_id(6)],
|
||||
..Default::default()
|
||||
};
|
||||
reconcile_parameter_card(&mut state, &view);
|
||||
assert_eq!(state.card.action, Some(Action::Extrude));
|
||||
state.card.buffers[0] = "7.".into();
|
||||
field_changed(&mut state, 0);
|
||||
let owner = subject(&state, &view, Action::Extrude);
|
||||
state.picked_curves.reverse();
|
||||
view.sketch_curves.reverse();
|
||||
view.sketch_curves[1].radius_mm = Some(8.0); // Geometry/source refresh.
|
||||
reconcile_parameter_card(&mut state, &view);
|
||||
assert_eq!(subject(&state, &view, Action::Extrude), owner);
|
||||
assert_eq!(state.card.buffers, ["7."]);
|
||||
view.sketch_curves[0].construction = true;
|
||||
reconcile_parameter_card(&mut state, &view);
|
||||
assert_ne!(state.card.action, Some(Action::Extrude));
|
||||
assert!(!state.card.buffers.contains(&"7.".to_owned()));
|
||||
// Structural ownership is specific to Extrude. Existing radius edits
|
||||
// retain their established curve identity across construction changes.
|
||||
state.picked_curves = vec![test_id(6)];
|
||||
reconcile_parameter_card(&mut state, &view);
|
||||
state.card.buffers[0] = "4.".into();
|
||||
field_changed(&mut state, 0);
|
||||
view.sketch_curves[1].construction = true;
|
||||
view.sketch_curves[1].endpoints = Some((test_id(4), test_id(3)));
|
||||
reconcile_parameter_card(&mut state, &view);
|
||||
assert_eq!(state.card.buffers, ["4."]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parameter_sources_preserve_precise_existing_values_and_angle_bits() {
|
||||
let exact = f64::from_bits(0x3ff3_c0ca_428c_59fb);
|
||||
@@ -951,6 +1111,7 @@ mod tests {
|
||||
};
|
||||
let view = SceneView {
|
||||
sketch_curves: vec![SketchCurveRow {
|
||||
endpoints: None,
|
||||
radius_mm: Some(2.0),
|
||||
id: test_id(2),
|
||||
sketch: test_id(1),
|
||||
@@ -1006,6 +1167,7 @@ mod tests {
|
||||
};
|
||||
let mut view = SceneView {
|
||||
sketch_curves: vec![SketchCurveRow {
|
||||
endpoints: None,
|
||||
radius_mm: Some(1.23456789123456),
|
||||
id: test_id(2),
|
||||
sketch: test_id(1),
|
||||
|
||||
@@ -1518,6 +1518,7 @@ fn readout_group(
|
||||
.and_then(|id| view.timeline.iter().find(|row| row.id == id))
|
||||
.map_or_else(|| "1 feature".to_owned(), |row| row.label.clone()),
|
||||
Selection::Circle => "1 circle".to_owned(),
|
||||
Selection::ConstructionCircle => "1 construction circle".to_owned(),
|
||||
Selection::Line => "1 line".to_owned(),
|
||||
Selection::Arc => "1 arc".to_owned(),
|
||||
Selection::SketchDimension { kind } => match kind {
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
//! Explicit existing-curve profile selection, independent of drawing state.
|
||||
|
||||
use super::{ClosedProfile, DocEntityId, SceneView, ShellState, SketchCurveKind, SketchCurveRow};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
/// Recognizes exactly one explicitly selected loop from immutable scene rows.
|
||||
/// Coordinates do not participate: only live curve IDs and endpoint identities.
|
||||
/// This is readiness, not geometry validation; the document validates again.
|
||||
#[must_use]
|
||||
pub fn selected_profile(rows: &[SketchCurveRow], picked: &[DocEntityId]) -> Option<ClosedProfile> {
|
||||
let mut selected = BTreeMap::new();
|
||||
for id in picked {
|
||||
let mut matches = rows.iter().filter(|row| row.id == *id);
|
||||
let row = matches.next()?;
|
||||
if matches.next().is_some() || row.construction || selected.insert(*id, row).is_some() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let (&first_id, first) = selected.first_key_value()?;
|
||||
if selected.values().any(|row| row.sketch != first.sketch) {
|
||||
return None;
|
||||
}
|
||||
if selected.len() == 1 && first.kind == SketchCurveKind::Circle {
|
||||
return Some(ClosedProfile {
|
||||
sketch: first.sketch,
|
||||
curves: vec![first_id],
|
||||
});
|
||||
}
|
||||
if selected.len() < 2
|
||||
|| selected
|
||||
.values()
|
||||
.any(|row| row.kind == SketchCurveKind::Circle)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut incident: BTreeMap<DocEntityId, Vec<DocEntityId>> = BTreeMap::new();
|
||||
for (&id, row) in &selected {
|
||||
let (a, b) = row.endpoints?;
|
||||
if a == b {
|
||||
return None;
|
||||
}
|
||||
incident.entry(a).or_default().push(id);
|
||||
incident.entry(b).or_default().push(id);
|
||||
}
|
||||
if incident.values().any(|edges| edges.len() != 2) {
|
||||
return None;
|
||||
}
|
||||
// ID chooses only the start. The stored direction of that first edge
|
||||
// chooses the winding; every remaining edge is found by connectivity.
|
||||
let (start, mut at) = first.endpoints?;
|
||||
let mut curves = vec![first_id];
|
||||
let mut visited = BTreeSet::from([first_id]);
|
||||
while curves.len() < selected.len() {
|
||||
let mut next = incident
|
||||
.get(&at)?
|
||||
.iter()
|
||||
.filter(|id| !visited.contains(*id));
|
||||
let id = *next.next()?;
|
||||
if next.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
let (a, b) = selected.get(&id)?.endpoints?;
|
||||
at = if a == at {
|
||||
b
|
||||
} else if b == at {
|
||||
a
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
visited.insert(id);
|
||||
curves.push(id);
|
||||
}
|
||||
(at == start).then_some(ClosedProfile {
|
||||
sketch: first.sketch,
|
||||
curves,
|
||||
})
|
||||
}
|
||||
|
||||
/// The same profile candidate used by readiness and Extrude dispatch.
|
||||
/// An explicit selection is authoritative, including when it is invalid;
|
||||
/// it must never fall back to a different transient drawing chain.
|
||||
#[must_use]
|
||||
pub fn profile_for_extrude(state: &ShellState, view: &SceneView) -> Option<ClosedProfile> {
|
||||
if state.session.is_some()
|
||||
|| state.picked_dimension.is_some()
|
||||
|| !state.picked_points.is_empty()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if state.picked_curves.is_empty() {
|
||||
view.closed_profile.clone()
|
||||
} else {
|
||||
selected_profile(&view.sketch_curves, &state.picked_curves)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
use super::super::tests::test_id;
|
||||
use super::super::*;
|
||||
|
||||
fn line(id: usize, a: usize, b: usize) -> SketchCurveRow {
|
||||
SketchCurveRow {
|
||||
radius_mm: None,
|
||||
endpoints: Some((test_id(a), test_id(b))),
|
||||
id: test_id(id),
|
||||
sketch: test_id(1),
|
||||
label: "line".into(),
|
||||
kind: SketchCurveKind::Line,
|
||||
construction: false,
|
||||
defining_points: vec![test_id(a), test_id(b)],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_profile_existing_rectangle_uses_the_whole_explicit_selection() {
|
||||
// Both row and pick order differ from the connected walk. Entity IDs
|
||||
// intentionally have no geometric ordering significance.
|
||||
let view = SceneView {
|
||||
sketch_curves: vec![line(8, 3, 4), line(6, 2, 3), line(7, 5, 2), line(9, 4, 5)],
|
||||
..Default::default()
|
||||
};
|
||||
let state = ShellState {
|
||||
picked_curves: vec![test_id(9), test_id(6), test_id(7), test_id(8)],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(selection_of(&state, &view), Selection::Profile);
|
||||
assert!(
|
||||
crate::toolbar::actions_for(selection_of(&state, &view)).contains(&Action::Extrude)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn selected_profile_order_is_a_connected_walk_independent_of_rows_and_picks() {
|
||||
let mut rows = vec![line(8, 3, 4), line(6, 2, 3), line(7, 5, 2), line(9, 4, 5)];
|
||||
let expected = ClosedProfile {
|
||||
sketch: test_id(1),
|
||||
curves: [6, 8, 9, 7].map(test_id).to_vec(),
|
||||
};
|
||||
for a in 6..10 {
|
||||
for b in 6..10 {
|
||||
for c in 6..10 {
|
||||
for d in 6..10 {
|
||||
let picked = [a, b, c, d].map(test_id);
|
||||
if std::collections::BTreeSet::from(picked).len() == 4 {
|
||||
assert_eq!(selected_profile(&rows, &picked), Some(expected.clone()));
|
||||
rows.reverse();
|
||||
assert_eq!(selected_profile(&rows, &picked), Some(expected.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_profile_accepts_arc_line_spline_and_two_arc_closures() {
|
||||
for kinds in [
|
||||
[SketchCurveKind::Arc, SketchCurveKind::Line],
|
||||
[SketchCurveKind::Spline, SketchCurveKind::Line],
|
||||
[SketchCurveKind::Arc, SketchCurveKind::Arc],
|
||||
] {
|
||||
let mut rows = vec![line(6, 2, 3), line(7, 3, 2)];
|
||||
rows[0].kind = kinds[0];
|
||||
rows[1].kind = kinds[1];
|
||||
// Defining points may contain a centre/interior fits. Only the
|
||||
// explicit endpoint pair participates in profile recognition.
|
||||
rows[0].defining_points = [4, 2, 5, 3].map(test_id).to_vec();
|
||||
assert_eq!(
|
||||
selected_profile(&rows, &[test_id(7), test_id(6)])
|
||||
.unwrap()
|
||||
.curves,
|
||||
[test_id(6), test_id(7)]
|
||||
);
|
||||
let state = ShellState {
|
||||
picked_curves: vec![test_id(6), test_id(7)],
|
||||
..Default::default()
|
||||
};
|
||||
let view = SceneView {
|
||||
sketch_curves: rows,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(selection_of(&state, &view), Selection::Profile);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_profile_rejects_the_entire_invalid_set_without_transient_fallback() {
|
||||
let rectangle = vec![line(8, 3, 4), line(6, 2, 3), line(7, 5, 2), line(9, 4, 5)];
|
||||
let cases = [
|
||||
(rectangle.clone(), vec![]),
|
||||
(rectangle.clone(), vec![6]),
|
||||
(rectangle.clone(), vec![6, 8, 9]),
|
||||
(rectangle.clone(), vec![6, 8, 9, 7, 6]),
|
||||
(rectangle.clone(), vec![6, 8, 9, 42]),
|
||||
(
|
||||
{
|
||||
let mut r = rectangle.clone();
|
||||
r[0].construction = true;
|
||||
r
|
||||
},
|
||||
vec![6, 8, 9, 7],
|
||||
),
|
||||
(
|
||||
{
|
||||
let mut r = rectangle.clone();
|
||||
r[0].sketch = test_id(20);
|
||||
r
|
||||
},
|
||||
vec![6, 8, 9, 7],
|
||||
),
|
||||
(
|
||||
{
|
||||
let mut r = rectangle.clone();
|
||||
r.push(r[0].clone());
|
||||
r
|
||||
},
|
||||
vec![6, 8, 9, 7],
|
||||
),
|
||||
(
|
||||
{
|
||||
let mut r = rectangle.clone();
|
||||
r[0].endpoints = None;
|
||||
r
|
||||
},
|
||||
vec![6, 8, 9, 7],
|
||||
),
|
||||
(
|
||||
{
|
||||
let mut r = rectangle.clone();
|
||||
r.push(line(10, 2, 11));
|
||||
r
|
||||
},
|
||||
vec![6, 8, 9, 7, 10],
|
||||
),
|
||||
(
|
||||
{
|
||||
let mut r = rectangle.clone();
|
||||
r.extend([line(10, 11, 12), line(13, 12, 11)]);
|
||||
r
|
||||
},
|
||||
vec![6, 8, 9, 7, 10, 13],
|
||||
),
|
||||
(
|
||||
{
|
||||
let mut r = rectangle.clone();
|
||||
r[0].kind = SketchCurveKind::Circle;
|
||||
r[0].endpoints = None;
|
||||
r
|
||||
},
|
||||
vec![6, 8, 9, 7],
|
||||
),
|
||||
(
|
||||
vec![line(6, 2, 3), line(7, 3, 2), line(8, 2, 3)],
|
||||
vec![6, 7, 8],
|
||||
),
|
||||
(vec![line(6, 2, 2), line(7, 2, 3)], vec![6, 7]),
|
||||
];
|
||||
for (rows, picks) in cases {
|
||||
let picked: Vec<_> = picks.into_iter().map(test_id).collect();
|
||||
assert!(
|
||||
selected_profile(&rows, &picked).is_none(),
|
||||
"invalid picks {picked:?}"
|
||||
);
|
||||
if !picked.is_empty() {
|
||||
let state = ShellState {
|
||||
picked_curves: picked,
|
||||
..Default::default()
|
||||
};
|
||||
let view = SceneView {
|
||||
sketch_curves: rows,
|
||||
closed_profile: Some(ClosedProfile {
|
||||
sketch: test_id(1),
|
||||
curves: vec![test_id(99)],
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(profile_for_extrude(&state, &view).is_none());
|
||||
assert!(
|
||||
!crate::toolbar::actions_for(selection_of(&state, &view))
|
||||
.contains(&Action::Extrude)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_profile_keeps_circle_radius_and_open_line_constraint_controls() {
|
||||
let mut circle = line(6, 2, 3);
|
||||
circle.kind = SketchCurveKind::Circle;
|
||||
circle.endpoints = None;
|
||||
let mut view = SceneView {
|
||||
sketch_curves: vec![circle],
|
||||
..Default::default()
|
||||
};
|
||||
let state = ShellState {
|
||||
picked_curves: vec![test_id(6)],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(selection_of(&state, &view), Selection::Circle);
|
||||
assert_eq!(
|
||||
profile_for_extrude(&state, &view).unwrap().curves,
|
||||
vec![test_id(6)]
|
||||
);
|
||||
view.sketch_curves[0].construction = true;
|
||||
assert_eq!(selection_of(&state, &view), Selection::ConstructionCircle);
|
||||
assert!(profile_for_extrude(&state, &view).is_none());
|
||||
let actions = crate::toolbar::actions_for(selection_of(&state, &view));
|
||||
assert!(actions.contains(&Action::EditDimension));
|
||||
assert!(actions.contains(&Action::ToggleConstruction));
|
||||
assert!(!actions.contains(&Action::Extrude));
|
||||
let view = SceneView {
|
||||
sketch_curves: vec![line(6, 2, 3), line(7, 3, 4)],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(selection_of(&state, &view), Selection::Line);
|
||||
let state = ShellState {
|
||||
picked_curves: vec![test_id(6), test_id(7)],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(selection_of(&state, &view), Selection::TwoLines);
|
||||
}
|
||||
}
|
||||
@@ -1314,6 +1314,7 @@ fn a_face_card_does_not_float_over_the_sketch_being_drawn_on_it() {
|
||||
let mut view = view();
|
||||
// One circle to pick, so the third case has a SKETCH card to keep.
|
||||
view.sketch_curves.push(SketchCurveRow {
|
||||
endpoints: None,
|
||||
radius_mm: Some(1.0),
|
||||
id: circle,
|
||||
sketch: view.timeline[0].id,
|
||||
@@ -1505,6 +1506,7 @@ fn a_ribbon_press_aims_the_card_and_enter_runs_it_with_the_typed_number() {
|
||||
let points = [test_id(11), test_id(12)];
|
||||
let mut view = view();
|
||||
view.sketch_curves.push(SketchCurveRow {
|
||||
endpoints: None,
|
||||
radius_mm: Some(1.0),
|
||||
id: circle,
|
||||
sketch: view.timeline[0].id,
|
||||
@@ -2206,6 +2208,7 @@ fn parameter_actual_enter_keeps_creation_and_section_requests_enabled() {
|
||||
let ctx = shell_ctx();
|
||||
let mut scene = view();
|
||||
scene.sketch_curves.push(SketchCurveRow {
|
||||
endpoints: None,
|
||||
radius_mm: Some(3.0),
|
||||
id: test_id(30),
|
||||
sketch: test_id(1),
|
||||
|
||||
@@ -152,6 +152,7 @@ fn card_display_never_touches_the_document() {
|
||||
fn a_picked_arc_offers_its_radius_and_a_spline_still_offers_nothing() {
|
||||
let sketch = test_id(1);
|
||||
let row = |id: usize, kind: SketchCurveKind| SketchCurveRow {
|
||||
endpoints: None,
|
||||
radius_mm: Some(1.0),
|
||||
id: test_id(id),
|
||||
sketch,
|
||||
|
||||
@@ -22,6 +22,8 @@ pub enum Selection {
|
||||
Feature(FeatureKind),
|
||||
/// One sketch circle.
|
||||
Circle,
|
||||
/// A construction circle retains radius editing but cannot be a profile.
|
||||
ConstructionCircle,
|
||||
/// One sketch line.
|
||||
///
|
||||
/// Added when constraints got a UI path. A line was previously reported
|
||||
@@ -891,6 +893,7 @@ pub fn actions_for(selection: Selection) -> Vec<Action> {
|
||||
// M3 lane B appends `ToggleConstruction` to both single-curve
|
||||
// arms: a construction curve takes every constraint a profile one
|
||||
// does, so the toggle belongs wherever a lone curve is picked.
|
||||
Selection::ConstructionCircle => vec![Action::EditDimension, Action::ToggleConstruction],
|
||||
Selection::Circle => vec![
|
||||
Action::EditDimension,
|
||||
Action::Extrude,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Existing profile control verification — 2026-09-08
|
||||
|
||||
## Scope
|
||||
|
||||
This gate verifies reuse of one explicitly selected, already-authored line loop after two state-resetting workflows:
|
||||
|
||||
- creating a rectangle, pressing real Ctrl+Z and Ctrl+Y, and selecting its restored sides;
|
||||
- saving the rectangle through the title bar, dirtying the document with real Ctrl+Z, reopening it through the `$` command bar and `discard and open`, and selecting its reopened sides.
|
||||
|
||||
Both scripts select the rectangle in the deliberately non-walk order `[8, 6, 9, 7]` (top, bottom, left, right). The expected profile stored in the Extrude feature is the deterministic connected walk `[6, 7, 8, 9]`. Recognizing `profile` naturally paints the Extrude height card; the scripts type and submit that real card and use real keyboard shortcuts. Pressing the ribbon command again would immediately submit the already-visible card at its current value, so it is not part of the natural creation route. The driver `save` and `export_step` steps are used only as native-document and exported-geometry oracles.
|
||||
|
||||
This is one-loop reuse coverage. It does not add planar-region or nested-loop semantics, and it does not claim that an arbitrary set of curves is a valid profile.
|
||||
|
||||
## Frozen baseline failure
|
||||
|
||||
The pre-repair binary was copied before implementation began:
|
||||
|
||||
```text
|
||||
/tmp/vernier-drive-existing-profile-f56
|
||||
SHA-256 5207fc230e7d463cc810240451387c2a8ddf37b543ad0ed7e0bc73add22e2e7e
|
||||
source checkpoint f56bb172dc258f85188edf302a81c8a7397c4602
|
||||
```
|
||||
|
||||
Both positive scripts fail on that binary at their first repaired assertion, before Extrude is invoked:
|
||||
|
||||
- `scripts/drive/existing-profile-redo.json` exits 1 at step 28. The shell reports selection `line`, expected `profile`, with exact picked curves `[8, 6, 9, 7]`. Report: `/tmp/vernier-existing-profile-redo-f56-final-red/report.json`; failure bundle: `/tmp/vernier-existing-profile-redo-f56-final-red/run1/failure/`.
|
||||
- `scripts/drive/existing-profile-open.json` exits 1 at step 42 with the same selection mismatch and exact picked curves. Before that point it successfully saves, dirties, reopens, reports `opened …/rectangle.vernier`, and restores the four exact native line records. Report: `/tmp/vernier-existing-profile-open-f56-final-red/report.json`; failure bundle: `/tmp/vernier-existing-profile-open-f56-final-red/run1/failure/`.
|
||||
|
||||
The failure is therefore profile classification of a real accumulated existing-curve selection. It is not loss of curves, failure to extend selection with Ctrl, use of the wrong workspace, or an unpainted-card proxy.
|
||||
|
||||
## Acceptance oracles
|
||||
|
||||
The rectangle corners are `(-10,-5)`, `(10,-5)`, `(10,5)`, and `(-10,5)` mm, so a 2 mm straight extrusion must have
|
||||
|
||||
```text
|
||||
V = 20 mm × 10 mm × 2 mm = 400 mm³
|
||||
```
|
||||
|
||||
Each repaired run must assert one solid, six faces, and volume 400 mm³ at relative tolerance `1e-9`. The four exact authored corner coordinates have horizontal/vertical inference but no dimensional or positional constraints, so the expected and asserted compile warning is `sketch entity#1: 4 free degrees of freedom`; it does not weaken the fixed-coordinate exported-geometry oracle. The native document must store sketch `1`, profile `[6,7,8,9]`, height `2.0`, and new-body identity `11`. After creation, real Ctrl+Z must remove only the Extrude timeline entry while the still-live explicit loop remains selected as `profile`; real Ctrl+Y must restore the two-feature document, preserve that profile selection, and recover the same native references and exported solid. Created and redone STEP bytes are compared outside the script as an additional determinism check.
|
||||
|
||||
## Repaired release evidence
|
||||
|
||||
The final repaired release was copied and frozen before execution:
|
||||
|
||||
```text
|
||||
/tmp/vernier-drive-existing-profile-final
|
||||
SHA-256 0b6ab93b276e48f19eaaea9da0a56c16ccd7a1a35a55d5d02902f3062f22dcbb
|
||||
```
|
||||
|
||||
Both scripts pass with `--skip-png --require-adapter llvmpipe` in the driver's default cross-process mode:
|
||||
|
||||
- Redo route: `/tmp/vernier-existing-profile-redo-final/report.json`, 63 steps, 87 frames, process IDs `[12,71]`, `deterministic: true`.
|
||||
- Save/Open route: `/tmp/vernier-existing-profile-open-final/report.json`, 77 steps, 121 frames, process IDs `[12,71]`, `deterministic: true`.
|
||||
|
||||
Every created and redone native document is byte-identical within and across both runs. Its Extrude payload is:
|
||||
|
||||
```json
|
||||
{"sketch":1,"profile":[6,7,8,9],"height":2.0,"target":{"NewBody":{"body":11}}}
|
||||
```
|
||||
|
||||
Every created and redone STEP file is also byte-identical within and across both scripts and processes:
|
||||
|
||||
```text
|
||||
length 15342 bytes
|
||||
FNV-1a-64 0x5da43da129fbf979
|
||||
SHA-256 256b4a8fafe2d85238f02690a5986928bc2cd538641755998c4f90129f4185fd
|
||||
```
|
||||
|
||||
The imported STEP oracle measures `399.9999999999999 mm³`, six faces, and one solid; the analytic assertion is 400 mm³ at relative tolerance `1e-9`. Real Ctrl+Z returns the timeline to its one sketch feature and retains selection `profile`; real Ctrl+Y restores the Extrude and the exact bytes above.
|
||||
|
||||
Two false-oracle controls change only the first expected volume from 400 to 401 mm³. Both exit 1 at that geometry assertion, after their full recovery and creation route:
|
||||
|
||||
- `/tmp/vernier-existing-profile-redo-wrong.json` fails at step 43; report `/tmp/vernier-existing-profile-redo-final-wrong/report.json`.
|
||||
- `/tmp/vernier-existing-profile-open-wrong.json` fails at step 57; report `/tmp/vernier-existing-profile-open-final-wrong/report.json`.
|
||||
|
||||
Both messages report actual `399.9999999999999`, expected `401`, relative error `2.494e-3 > 1e-9`. This shows that the geometry oracle can reject a wrong solid volume independently of profile classification and document-reference assertions.
|
||||
|
||||
## Deliberate limits
|
||||
|
||||
- The profile is one explicit connected line component. Disconnected sets, branches, construction geometry, mixed circle-and-segment selections, and nested loops remain refusal or later-region cases.
|
||||
- The existing one-circle fallback is outside these two scripts. This gate does not replace its dedicated coverage or make a radius claim from freehand pixels.
|
||||
- Save and STEP export bypass no profile-selection or Extrude creation control; they observe the document and body after the GUI operations.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Existing selected profile reuse — implementation
|
||||
|
||||
Worktree `target/worktrees/reliability`, based on `f56bb172dc258f85188edf302a81c8a7397c4602`. This is the explicitly selected, single-loop slice of R7. No dependencies, native format changes, generic Document execution changes, commits, main edits or AGENTS edits were made by this implementation lane.
|
||||
|
||||
## Behavior and boundaries
|
||||
|
||||
`selected_profile` in `vernier-ui/src/shell/selected_profile.rs` recognizes the exact selected set from immutable `SketchCurveRow`s. Each row now carries the endpoint pair returned by `SketchCurve::endpoints`, so the UI does not duplicate Arc centre/start/end or spline fit-point indexing. IDs must be live and unique, every curve must belong to the same sketch, and construction geometry is not skipped. A lone Circle remains a profile; Circle mixed with anything else refuses. Lines, arcs and splines need distinct endpoints, degree two at every endpoint and one connected component containing every selected curve.
|
||||
|
||||
The minimum curve ID chooses the start only. Its authored endpoint direction chooses a winding; the rest of the order follows the unique unused incident curve. A test whose connected order is `[6,8,9,7]` proves this is not sorted-ID traversal. All 24 pick permutations and reversed scene row order produce that same result. Two curves sharing both endpoints remain allowed, including Arc+Line, Spline+Line and two arcs.
|
||||
|
||||
`profile_for_extrude` is shared by selection/readiness, Extrude dispatch and the Extrude parameter subject. A nonempty explicit curve selection is authoritative even when invalid: it cannot fall back to a different transient drawing chain or first picked circle. With no explicit selection, the existing drawing-chain route remains. The repair does not restore `sketch_active`, `sketch_pending` or `sketch_chain` on Undo/Redo/Open.
|
||||
|
||||
Valid multi-curve selections report `Profile` before the old one/two-curve constraint classifications. Invalid larger sets report no actionable selection; valid open one/two-line sets retain Line/TwoLines and their constraint actions. Ordinary Circle keeps its radius card. `ConstructionCircle` is a distinct selection with radius editing and construction toggle but no Extrude; the ribbon says `1 construction circle` and the driver spelling is `construction-circle`.
|
||||
|
||||
The Extrude card subject uses the canonical resolved profile plus its selected curve endpoint/construction metadata. Different pick orders of the same loop and coordinate/radius refreshes preserve a dirty height draft. Invalid or structurally changed candidates retire it. This metadata applies only to Extrude; it does not broaden the ownership of existing Circle/Arc radius drafts.
|
||||
|
||||
## Worker admission and solved geometry
|
||||
|
||||
`AddExtrude` previously reserved IDs and appended a feature without checking the profile; the compiler eventually refused stale inputs. The server also cleared drawing state before that attempt. `apply_features::ExtrudeSketch` now checks the sketch payload, calls `validate_profile_structure`, and applies the same selected-set graph rule against current worker rows before target resolution or command execution. A successful command retires drawing state; structural refusal leaves allocator, undo/redo, dirty state and pending drawing data intact.
|
||||
|
||||
The narrow document helper is factored from existing profile validation: construction, curve/defining-point liveness, repetition and the existing directed walk/closure logic. `validate_profile` still checks geometry before walking, preserving its existing error precedence. A multiply-invalid collapsed/disconnected test pins that distinction.
|
||||
|
||||
The preflight deliberately does not interpret authored coordinates as solved geometry and does not run an extra solve. Constraints can expand initially coincident authored points; projected references start at zero and obtain their actual positions in the compiler. Full `validate_profile` remains on the resolved, solved geometry before kernel construction. This lane does not move native compilation out of process or claim that a structural admission check proves geometric validity.
|
||||
|
||||
## Retained red/green evidence
|
||||
|
||||
- `/tmp/vernier-existing-profile-ui-red.log`: new rectangle classification test fails with `Line` versus `Profile`; `/tmp/vernier-existing-profile-ui-green.log` passes the same test after wiring.
|
||||
- `/tmp/vernier-existing-profile-server-red.log`: stale curve admission returns `Ok(Command)` rather than refusal; `/tmp/vernier-existing-profile-server-green.log` passes after preflight. An initial test fixture type error was corrected before recording the retained product red.
|
||||
- `/tmp/vernier-existing-profile-doc.log`: both structural/solved validation tests pass.
|
||||
- `/tmp/vernier-existing-profile-ui-expanded.log`: six graph/draft tests pass, covering canonical walk, all pick permutations, arc/spline closures, duplicate/missing rows and picks, mixed sketches, construction, self-loop/branch/disconnected components, mixed circles, no transient fallback, unchanged constraint controls and draft ownership.
|
||||
- App tests in `tests/existing_profiles.rs` cover actual Edit dispatch into real server/kernel geometry for Arc+Line and Spline+Line; exact curve IDs; Undo/Redo; stale snapshot refusals for missing defining points, construction, changed endpoints, a degree-four revisit and wrong sketch payload; full session-byte/redo/allocator preservation; and invalid explicit selections with a valid transient fallback present.
|
||||
- The projected-coordinate control uses three actual projected face centroids, all stored at authored `(0,0)`, joined into a triangle on the block's top face. The regular compiler resolves the points and a 2 mm extrusion yields 18,300 mm³ (18,000 + 150×2). This verifies that preflight does not incorrectly reject unresolved projections.
|
||||
- The solver-expanded control starts all four authored rectangle points at zero with exact Lock targets. The final volume is `399.999999970413114`, matching an independent shoelace calculation from solved vertices within `1e-8` absolute and the ideal 400 within `1e-9` relative. The initial new test's `1e-8` absolute comparison to ideal 400 was too strict for that solver residual; `/tmp/vernier-existing-profile-solved-oracle.log` retains the calibration failure. No existing oracle was weakened.
|
||||
|
||||
## Verification commands
|
||||
|
||||
All commands ran in the worktree with `CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target`, behind 180 or 240 second process deadlines.
|
||||
|
||||
| Command | Outcome |
|
||||
| --- | --- |
|
||||
| `cargo test -p vernier-doc -p vernier-ui -p vernier-app --lib` | Doc 294, app 329 and UI 344 passed; 3 existing UI tests ignored. `/tmp/vernier-existing-profile-libraries.log` |
|
||||
| `cargo clippy -p vernier-doc -p vernier-ui -p vernier-app -p vernier-drive --all-targets -- -D warnings` | Exit 0. `/tmp/vernier-existing-profile-clippy-final.log` |
|
||||
| `cargo build --release -p vernier-drive` | Exit 0. `/tmp/vernier-existing-profile-release-final.log` |
|
||||
| `git diff --check` | Clean. |
|
||||
|
||||
Pre-resume release SHA256: `0b6ab93b276e48f19eaaea9da0a56c16ccd7a1a35a55d5d02902f3062f22dcbb`. The resumed correction and current release evidence below supersede this build.
|
||||
|
||||
Actual shell Undo/Redo/Open controls and STEP oracles are authored and run by the separate `drive_solid_builders` lane, recorded in `EXISTING_PROFILE_CONTROL_VERIFICATION_2026-09-08.md`. That report distinguishes the natural already-visible profile card from a redundant ribbon press, which would immediately submit the existing card. Root owns script registration and the final combined gate.
|
||||
|
||||
Final actual-control reports on that exact release binary pass cross-process determinism:
|
||||
|
||||
- `/tmp/vernier-existing-profile-redo-final/report.json`: 63 steps, 87 frames.
|
||||
- `/tmp/vernier-existing-profile-open-final/report.json`: 77 steps, 121 frames.
|
||||
|
||||
Both select all four sides in non-walk pick order, assert `profile`, use the naturally painted height card and actual Enter, and assert exact native sketch/profile/height/body references plus one solid, six faces and 400 mm³ at `1e-9` relative tolerance. Actual Undo and Redo restore the same native references and STEP bytes. The companion control report retains the frozen f56 baseline reds at selection steps 28 and 42, and wrong-volume negative controls that fail at the intended geometry assertion. Native save/export helper steps serve only as explicit document/geometry oracles; the Open route itself uses actual controls.
|
||||
|
||||
|
||||
## Resumed cut-card regression correction
|
||||
|
||||
The combined workspace run exposed a regression that the earlier selected-loop controls did not cover: unchanged `cut-then-boss.json` failed at step 27 because `chip:cut` was absent after radius editing and an Extrude ribbon press. This was reproduced after the SSD pause on the retained release binary; `/tmp/vernier-profile-cut-red/report.json` and its sibling `.log` retain the failure. The preceding trace shows a Circle selection throughout radius editing and the Extrude press, with no new feature dispatched.
|
||||
|
||||
The cause was draft reconciliation, not profile recognition or Cut dispatch. `Subject` now contains action-specific data: Extrude adds its effective ordered profile and endpoint/construction metadata, while radius editing keeps its existing ownership rules. The action-switch check cloned the new Extrude subject and replaced only its `action` field before comparing it to the old radius draft. That could never match the radius subject's different projection; reconciliation cancelled the explicit Extrude aim and returned to the radius card.
|
||||
|
||||
`reconcile_parameter_card` now recomputes the current subject using the **previous action's** rules before deciding whether to preserve a newly requested aim. This retains an intentional command switch for the same subject while still retiring the aim and dirty text when the previous subject changes. Profile recognition, explicit invalid-set refusal and server admission are unchanged by this correction.
|
||||
|
||||
The new `parameter_circle_radius_can_aim_extrude_without_losing_its_subject` regression failed first with `armed: None` versus `Some(Extrude)` (`/tmp/vernier-profile-cut-ui-red.log`). It then passed with the correction alongside all 19 parameter-filtered tests (`/tmp/vernier-profile-cut-ui-green.log`). The regression covers a selected circle both with and without a transient drawing profile, radius → Extrude → radius, exact radius bits, dirty height and Cut preservation during source refresh, changed-circle aim retirement, and construction conversion retiring the height draft. Existing canonical-profile and structural-invalidation tests continue to pass.
|
||||
|
||||
Fresh verification, all with bounded commands and the shared target directory:
|
||||
|
||||
| Command / actual workflow | Result and retained evidence |
|
||||
| --- | --- |
|
||||
| `cargo test -p vernier-doc -p vernier-ui -p vernier-app --lib` | App 329, doc 294, UI 345 passed; 3 existing UI tests ignored. `/tmp/vernier-profile-cut-libraries.log`. |
|
||||
| `cargo clippy -p vernier-doc -p vernier-ui -p vernier-app -p vernier-drive --all-targets -- -D warnings` | Exit 0, `/tmp/vernier-profile-cut-clippy.log`. |
|
||||
| `cargo build --release -p vernier-drive` | Exit 0, `/tmp/vernier-profile-cut-release.log`. |
|
||||
| Unchanged `cut-then-boss.json` | Pass, 71 steps / 175 frames, cross-process deterministic; `/tmp/vernier-profile-cut-green/report.json`. STEP deltas remain −251.32741228718345 mm³ for the cut and +141.3716694115407 mm³ for the following boss, each at `1e-9` relative tolerance with one solid. |
|
||||
| Unchanged `existing-profile-redo.json` | Pass, 63 steps / 87 frames, cross-process deterministic; `/tmp/vernier-profile-cut-redo-green/report.json`. |
|
||||
| Unchanged `existing-profile-open.json` | Pass, 77 steps / 121 frames, cross-process deterministic; `/tmp/vernier-profile-cut-open-green/report.json`. |
|
||||
|
||||
The actual workflows ran on llvmpipe (LLVM 22.1.8, 256 bits). The current release binary SHA256 is `3ad9df4c2b7c2d81a17c6fb92dc8467a554db19f50e9bf7b3fb0e7f6e76ab75c`. The unchanged cut script SHA256 is `92ec3b9b7364306a2cb411e94cafaaf50fd2acae09546be86a022af26a9a2b68`. The existing-profile controls retain their native identity and 400 mm³ STEP oracles. Frame totals above are the reports' `frames` field; `frames_digest` has two fewer entries because it starts at frame 2. No script or oracle was changed for this correction. Root still owns the final workspace gate.
|
||||
|
||||
## Owned files and integration
|
||||
|
||||
New: UI `shell/selected_profile.rs`; app `tests/existing_profiles.rs` (registered in the existing test module and reachability list). Production changes: UI shell module/parameter subject/toolbar/ribbon; app edit mapping and server feature admission/scene row construction; doc `sketch.rs` helper plus `lib.rs` export; driver `expect.rs` exhaustive selection spelling. Existing row literals in UI shell tests and app cut/value-card tests were mechanically supplied with endpoint metadata (`None` for their existing circle or non-profile fixtures).
|
||||
|
||||
Root-owned `tests/scripts.rs`, `scripts/check.fish` and control scripts were not edited by this implementation lane. Future process transport needs to carry `SketchCurveRow::endpoints` and the new construction-circle selection vocabulary.
|
||||
|
||||
## Explicit limits
|
||||
|
||||
This recognizes exactly one selected connected component. It does not infer planar regions, select a subset, add nested-loop/hole payload semantics, solve self-intersections or implement multi-body selection. Geometry collapse and kernel-invalid shapes remain full compile-time validation, with the existing rollback behavior. The helper's structural closure alone allows some vertex revisits; server admission additionally applies the stricter degree-two selected graph rule. The current real-control scripts cover the selected rectangle after Undo/Redo and actual Open; Arc/Spline geometry evidence is a compiled document fixture through production dispatch, not a new scripted Arc/Spline authoring workflow.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Existing selected profile review — 2026-09-08
|
||||
|
||||
Independent read-only production review after checkpoint `f56bb17`, against `EXISTING_PROFILE_REOPEN_AUDIT_2026-09-08.md`. **No remaining blocker was found for the explicitly selected, single-loop repair.** Production and tests were frozen before independent verification. The reviewer owns only this report and has made no production/test changes or commits. This does not close all of R7: nested regions remain unfinished.
|
||||
|
||||
## Reviewed contract
|
||||
|
||||
The repair recognizes one explicitly selected connected component from immutable `SketchCurveRow` data. It does not reconstruct a Document on the render thread or restore transient authoring state after Undo/Open. Nested regions, holes and general planar-region inference remain separate unfinished work.
|
||||
|
||||
`selected_profile` rejects duplicate selections, duplicate/missing rows, construction curves, mixed sketches, circles mixed with other curves, missing or identical endpoints, non-degree-two incidence, and disconnected components. A minimum curve ID chooses the start; its authored direction chooses the winding. Each subsequent curve is found by endpoint identity and unused incidence, not by sorted entity IDs or selection order. Consuming every selected edge and returning to the starting endpoint proves the whole selected component was traversed. Two distinct curves sharing both endpoints correctly form a two-edge cycle. Circle, arc and spline geometry still requires the document/compiler's full validation.
|
||||
|
||||
`profile_for_extrude` makes any nonempty explicit curve selection authoritative. Invalid explicit picks cannot silently fall back to an unrelated transient drawing profile. `selection_of` uses this recognition before its two-curve/first-curve classifications, and `edit_for(Action::Extrude)` consumes the same result. A lone ordinary circle retains its radius-card selection. A construction circle retains radius editing and its construction toggle through an explicit selection variant, while Extrude is unavailable. Open one-line/two-line constraint selections retain their existing classifications.
|
||||
|
||||
## Review findings addressed during implementation
|
||||
|
||||
1. **Authored versus solved geometry:** calling the existing full `validate_profile` before mutation would reject valid authored-collapsed geometry that constraints later expand. The new `validate_profile_structure` reuses construction/liveness/repetition checks and the exact walk without inspecting authored coordinates as final geometry. Full `validate_profile` retains its previous geometry-before-walk error ordering and stays in the compiler after projection resolution and solving. No separate projection-blind solve is added.
|
||||
2. **Stale graph changes:** a closed directed walk can revisit a vertex even though the selected-profile incidence rule forbids branches. The worker therefore performs both structural ordered-chain validation and the same degree-two selected graph check on current rows before `AddExtrude` can allocate or record history.
|
||||
3. **Transient state mutation:** Extrude previously cleared drawing state before validating its request. Current admission precedes allocation/history, and drawing state retires after successful command execution. Geometry/kernel failures remain subject to the existing compile refusal/rollback policy; this is not a universal pre-kernel geometry guarantee.
|
||||
4. **Parameter draft ownership:** profile endpoint/construction metadata is restricted to Extrude. Applying it to every parameter card would change the established radius-draft lifecycle. Extrude uses the effective ordered candidate so equivalent pick permutations do not produce different owners; ordinary coordinate changes do not participate in the key.
|
||||
5. **Readiness parity:** construction circles and duplicate single-curve rows must not show a live Extrude action that the shared resolver refuses. Explicit construction metadata preserves the existing radius/constraint behavior without pretending a circle is an arc.
|
||||
|
||||
## Independent focused evidence
|
||||
|
||||
Run sequentially after the frozen release build, with `CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target`:
|
||||
|
||||
| Command | Result |
|
||||
| --- | --- |
|
||||
| `cargo test -p vernier-ui --lib selected_profile -- --nocapture` | Exit 0, 6 passed. `/tmp/vernier-existing-profile-review-ui.log` |
|
||||
| `cargo test -p vernier-app --lib existing_profile -- --nocapture` | Exit 0, 6 passed. `/tmp/vernier-existing-profile-review-app.log` |
|
||||
| `cargo test -p vernier-doc --lib profile_structure -- --nocapture` | Exit 0, 2 passed. `/tmp/vernier-existing-profile-review-doc.log` |
|
||||
|
||||
The pure UI tests cover the true walk `[6,8,9,7]` under all 24 pick permutations and reversed rows, two-edge Arc/Line, Spline/Line and Arc/Arc closures, complete invalid-set refusal despite a transient fallback, construction-circle behavior and open-line constraints. The draft test preserves typed height across equivalent pick permutations and radius/source refresh, retires a now-invalid profile, and preserves the prior radius-draft identity outside Extrude. These are immutable-row/helper tests, not viewport gesture evidence.
|
||||
|
||||
The app tests use production `edit_for` and document-server dispatch on explicit fixtures. Arc/Line and Spline/Line selections make real solids and retain volume through Undo/Redo. The semicircle has an analytic `25π` volume at height 2; the spline has a deliberately broad plausibility band, not an exact spline-volume oracle. Stale current-worker snapshots cover missing defining points, construction changes, changed endpoints, a degree-four vertex revisit and wrong sketch payload. They assert exact session bytes, allocator/history and redo preservation, unchanged dirty state and unchanged drawing sentinels before compilation. These damage fixtures are intentionally constructed through a test command; they do not claim every malformed snapshot can originate from a normal GUI gesture.
|
||||
|
||||
The solved-geometry controls are material to admission: a rectangle with all authored points at zero and nonzero Lock targets extrudes successfully; the full validator refuses its raw authored geometry while structural admission accepts it. Its volume agrees with an independent shoelace calculation of solved vertices within `1e-8` absolute and ideal 400 within `1e-9` relative. Three projected face-centroid points likewise remain authored at zero, then resolve through the normal compiler and extrude to total volume 18,300 mm³. No extra preflight solve is needed. Document tests pin both this structural/full distinction and the original geometry-before-topology error precedence.
|
||||
|
||||
Source locations reviewed: `shell/selected_profile.rs:10` and `:83`; `shell/mod.rs:2053`; `edit.rs:1210`; `server/apply_features.rs:213`; `vernier-doc/src/sketch.rs:824`, `:849`, `:858`; and `shell/parameter_card.rs:126`, `:157`. The compiler's full validation call sites were not replaced by the structural helper.
|
||||
|
||||
## Actual controls and instrument checks
|
||||
|
||||
Root and the control-verification agent authored and executed the real driven scripts. This reviewer read both final scripts, aggregate reports, error reports and artifact digests; the scripts were not independently rerun here.
|
||||
|
||||
| Evidence | Observed result |
|
||||
| --- | --- |
|
||||
| Final `existing-profile-redo.json` | `/tmp/vernier-existing-profile-redo-final/report.json`: pass, 63 steps/87 frames, deterministic cross-process, distinct process IDs, llvmpipe. |
|
||||
| Final `existing-profile-open.json` | `/tmp/vernier-existing-profile-open-final/report.json`: pass, 77 steps/121 frames, deterministic cross-process, distinct process IDs, llvmpipe. |
|
||||
| Same final scripts against frozen baseline | `/tmp/vernier-existing-profile-redo-f56-final-red/report.json` and `/tmp/vernier-existing-profile-open-f56-final-red/report.json`: fail at steps 28/42 respectively, actual selection `line` versus required `profile`, with all four exact picked IDs `[8,6,9,7]` present. |
|
||||
| Deliberately false STEP oracle | `/tmp/vernier-existing-profile-redo-final-wrong/report.json` and `/tmp/vernier-existing-profile-open-final-wrong/report.json`: fail at their own STEP steps 43/57, expected 401 versus actual approximately 400 mm³. |
|
||||
|
||||
Both positive scripts draw the rectangle through real controls, restore its authored state through actual Undo/Redo or actual title-bar Save and command-bar Open, and Ctrl-pick its four sides in a non-walk order. Recognition naturally paints the Extrude height card; real typing and Enter create the extrusion. A redundant Extrude ribbon click is not part of this final route and is not claimed as tested button coverage. Later Save/Export helper calls only observe native state and geometry.
|
||||
|
||||
The scripts assert exact stored profile `[6,7,8,9]`, sketch 1, height 2, new-body identity 11, one solid/six faces and 400 mm³ at relative tolerance `1e-9`. Actual Undo removes the extrusion while its still-live explicitly selected sketch loop remains selected; actual Redo restores it. Created/redone native document, sidecar and STEP digests match within each report and across the two workflows. The expected four-free-degrees-of-freedom sketch warning is asserted honestly; no claim is made that the freehand fixture is fully constrained.
|
||||
|
||||
Retained focused product reds are `/tmp/vernier-existing-profile-ui-red.log` (`Line` versus `Profile`) and `/tmp/vernier-existing-profile-server-red.log` (stale admission returned `Ok(Command)`). The solver-volume calibration failure is described separately in the implementation report; it is not presented as a production defect or a weakened preexisting oracle.
|
||||
|
||||
## Limits and disposition
|
||||
|
||||
All early review findings above have source corrections and focused regression coverage. The broader implementer library/Clippy results are recorded in `EXISTING_PROFILE_IMPLEMENTATION_2026-09-08.md`; root owns the final combined gate and checkpoint.
|
||||
|
||||
The actual viewport workflows prove selected rectangles after Undo/Redo and Open. Arc/spline solids, invalid selection graphs and parameter ownership use the narrower evidence levels stated above. There is no new driven negative gesture for every graph refusal, and no blanket claim that every profile or construction workflow is verified. Structural recognition does not prove area, absence of self-intersection or kernel validity; those remain full compile-time checks with existing rollback semantics. Nested/disconnected region semantics and multi-body target selection remain outside this repair.
|
||||
|
||||
Review report frozen on 2026-09-08. No further file writes or tests are planned before root's checkpoint.
|
||||
@@ -271,3 +271,11 @@ The selected-value repair closes the reproduced untouched PushPull overwrite: th
|
||||
Final review also found and corrected a Section card retirement bug that left the readout at7.5 mm but the rendered cut at0. The actual Headless ribbon/worker regression now asserts the retained shader plane after retirement, rearm and flip; the original PNG gate passes without a golden or threshold change. The new test runs even when the ordinary driver suite skips PNG checks.
|
||||
|
||||
The frozen combined gate passes **1,435 workspace tests,25 release Headless tests and44 selftests**, plus workspace/all-targets Clippy and formatting. Nine release workflows pass cross-process on the frozen binary: parameter editing, Cut/boss, actual files, existing dimensions, Section, Circular Pattern, Mirror, Arc and Polygon. Root's parameter false oracles fail exactly at unchanged volume step48 and exact native float step81, including a one-ULP difference at zero tolerance. Reports and source-hash boundaries are indexed in `IMPLEMENTATION_2026-09-08.md`. These totals include existing tests and do not certify every available operation, every parameter combination or native hang containment.
|
||||
|
||||
## Selected-profile reuse and natural dimension controls — after f56bb17
|
||||
|
||||
Selected rectangle profiles now extrude through their painted height card after actual Undo/Redo or Save/Open. Exact native profile IDs and400mm³/6faces/1solid exports survive actual extrusion Undo/Redo. The unchanged circle Cut/boss workflow also passes after the action-specific card ownership regression was found by the combined gate and corrected. See `EXISTING_PROFILE_IMPLEMENTATION_2026-09-08.md` and `PROFILE_CUT_REGRESSION_REVIEW_2026-09-08.md`; nested regions are not closed.
|
||||
|
||||
New actual-control workflows verify first Angle creation, existing number-label edits30→60degrees, analytic triangle extrusion, and Undo/Redo. Point Distance20→16 uses the real Measure control and asserts authored point3 remains10 while solved measurements are20/16. Both use full native constraint arrays; geometry/readout and native false assertions fail at their intended steps. Save/STEP helpers are oracles, not additional file-control credit. Natural repeated Length/Distance/Angle creation remains separately broken and has preserved red probes.
|
||||
|
||||
Final combined gate:1,451 workspace tests,25 release Headless tests,44 CLI selftests; Clippy/format pass. All28 regular modeling workflows pass plus the separate open-sketch point measurement test. Nine targeted release cross-process scripts pass with deterministic output on llvmpipe; original Section PNGs remain green. Source hashes and five false controls are recorded in the final checkpoint section of `IMPLEMENTATION_2026-09-08.md`. This adds bounded control coverage, not a claim that every registry function or native hang is covered.
|
||||
|
||||
@@ -184,3 +184,21 @@ No unsupported adapter may silently stand in for RADV golden verification. Repor
|
||||
- Circular Pattern102, Mirror63, Arc96 and Polygon83 also pass cross-process on that same frozen release; `/tmp/vernier-parameter-frozen-workflows.json` indexes these four and two deliberately false parameter oracles. A +1 mm³ unchanged-volume expectation fails at step48; a one-ULP native value difference (7.1234567890000005 instead of7.123456789) fails at step81 with tolerance0. All intended failure controls exit1 at their own assertions.
|
||||
- All cited release workflow runs pin llvmpipe. Section alone additionally passes its existing PNG assertions on that adapter; this is not RADV certification. The hash inventory of389 source/assets/scripts is unchanged across the final gate. Independent parameter, repeat-controls, Arc and Polygon reviews are closed. Main remains `b3f661f`; both user-owned AGENTS.md files retain their original SHA256.
|
||||
- The full goal remains open. Next: R7 selected existing line-loop extrusion after Undo/Open, then remaining region/multi-body/direct/history/sketch work and universal function coverage. Reusing the edge Length creation card on an already dimensioned line still appends a conflicting constraint; the existing dimension-label edit route works. Native process containment is not implemented. The updated codec plan includes new exact scene fields and parent acknowledgement ownership, while the reviewed export-staging brief prepares another prerequisite without claiming implementation. Main integration and app serde dependency edges retain their previously pending approvals.
|
||||
|
||||
### SSD pause and resumed selected-profile gate
|
||||
|
||||
- The user paused work for SSD replacement, then explicitly resumed. HEAD remains `f56bb172dc258f85188edf302a81c8a7397c4602`; the uncommitted selected-profile lane and its reports were preserved, and24 evidence files were recovered from the verified pause archive. No main integration occurred.
|
||||
- The pre-pause combined gate was correctly stopped with workspace exit101: `cut-then-boss` failed at step27 because the Cut chip was not painted. The prior narrow selected-profile review and two positive profile drives did not prove compatibility with circle-card aiming. This is a regression in the pending change, not a defect in the unchanged Cut script.
|
||||
- Reproduction after resume pinpoints parameter-card reconciliation: replacing only the action in an Extrude subject leaves its action-specific profile metadata, falsely making the prior Radius subject look like a different selection. The narrow correction compares the current selection through the previous action's subject rules. The new focused test covers circle Radius/Extrude switching with and without transient profile state, exact source restoration, dirty height/Cut preservation and actual selection/construction changes. Combined acceptance remains pending until the fresh gate below is complete.
|
||||
- The point-Distance positive workflow now includes an explicit authored point3 native assertion, addressing independent review. Its revised132-step result must be distinguished from the historical131-step evidence. Natural repeated Length/Distance/Angle remains unfixed; its concrete next repair brief and three preserved red probes remain in scope.
|
||||
|
||||
### Final selected-profile and dimension-control checkpoint gate
|
||||
|
||||
- Existing selected closed loops now extrude after actual Undo/Redo and actual Save/Open. Both readiness and dispatch use one immutable-row connected walk; worker structural admission refuses stale inputs before allocator/history/drawing-state mutation. Construction circles retain radius/toggle controls but do not offer Extrude. Nested regions remain unfinished R7 work.
|
||||
- The broad-gate circle-card regression was corrected and independently accepted in `PROFILE_CUT_REGRESSION_REVIEW_2026-09-08.md`. The unchanged cut-then-boss script reaches Cut again and proves the following boss remains additive. The focused regression fails on the broken card transition and passes after correction.
|
||||
- `/tmp/vernier-profile-resumed-final-gate-results.json`: formatting, workspace/all-targets Clippy with warnings denied, workspace tests, release Headless tests, release build and CLI selftest all exit0. Workspace **1,451 tests across66 suites**,19 existing ignored fixture/measurement cases, zero failures; release Headless **25/25**; CLI **44/44** checks. The regular modeling test now runs28 workflows; the open-sketch Distance/Measure workflow has a separate native/readout test.
|
||||
- All395 source/assets/script hashes are unchanged across this final gate (`/tmp/vernier-profile-resumed-gate-source-hashes.json`). Release binary SHA256 `3ad9df4c2b7c2d81a17c6fb92dc8467a554db19f50e9bf7b3fb0e7f6e76ab75c`. `/tmp/vernier-profile-resumed-frozen-drives.json` records nine deterministic cross-process passes: existing-profile-redo63 steps, existing-profile-open77, Angle116, point-Distance/Measure132, exact parameters110, cut-then-boss71, M3 dimensions94, actual files53 and Section29. All pin llvmpipe; only Section additionally checks its unchanged PNG goldens. No RADV certification is claimed.
|
||||
- `/tmp/vernier-profile-resumed-negatives.json`: all five one-assertion false controls exit1 at their intended steps. Profile volumes401 instead of400 fail at43/57; edited Angle volume+1 fails at99; Distance readout16.001 instead of16.000 fails at81; authored point11 instead of10 fails at38. These prove the fresh geometry/readout/native assertions observe the stated quantities.
|
||||
- `/tmp/vernier-profile-resumed-artifact-pairs.json` verifies20 within-run native/sidecar/STEP byte pairs across the two profile workflows, Angle and point-Distance history states. Every emitted artifact for those four workflows also matches between processes. The revised point script's authored-versus-solved assertion is now automated rather than dependent only on artifact review.
|
||||
- Main remains `b3f661fac0282dd5d02c5fc632b4343ca9ab18be`, and both user-owned AGENTS.md files retain SHA256 `9a45f280dc13af8888e4268461424ce77f08b47996211322231adc5d89b89bb5`. The current checkpoint is on the isolated feature branch only.
|
||||
- Full goal remains open: natural repeated Length/Distance/Angle still append conflicting constraints; the working number-label edit route does not close those failures. The reviewed next repair brief covers complete authored match metadata, solved-source availability, exact targets, semantic tokens, signed reverse angles, stale expected absence, and unchanged redo/history. Native process containment, remaining R1-R14 requirements, dependency-edge approval and main integration are not completed by these gates.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Natural dimension control review — 2026-09-08
|
||||
|
||||
## Verdict
|
||||
|
||||
The two positive scripts are valid actual-control evidence for first Angle creation, existing Angle label editing, first two-point Distance creation, solved-point Measure, and Distance label editing with Undo/Redo. Their formulas, exact native constraint arrays, positive reports, artifact pairs, and false-oracle failures agree. Save and STEP steps serve only as document and geometry oracles.
|
||||
|
||||
One small script/documentation mismatch and one automation gap remain in the point-distance gate:
|
||||
|
||||
1. The script notes and verification document say the two points are Ctrl-selected. The actual steps click the second point without `"ctrl": true` at steps 23, 69, 93, and 117. The traces show that the shell's real point-selection route accumulates the second point and reaches `two-points`, so this does not invalidate the tested control; the prose should say “select both points” rather than “Ctrl-pick”.
|
||||
2. The verification says the authored second point remaining at 10 mm is an assertion that distinguishes solved Measure from authored coordinates. The saved artifacts do contain point `3` as `[10.0,-0.0]` in every 20 mm and 16 mm document, and I inspected that evidence directly, but the committed script asserts only the full constraint arrays. Add an `expect_document` on `/state/features/1/payload/Sketch/points/3` equal to `[10.0,-0.0]` after Distance20 if this distinction must remain protected by the regular gate. As written, the Measure20/16 results and exact constraints prove the user-visible dimension workflow, while the authored-versus-solved distinction depends on review of the retained artifact.
|
||||
|
||||
No issue was found in the Angle control or analytic geometry evidence. The current positive reports use the frozen f56 binary; the verification document correctly requires rerunning them after the selected-profile repair before the combined checkpoint.
|
||||
|
||||
## Angle workflow
|
||||
|
||||
`scripts/drive/functions-sketch-angle.json` uses the real Sketch, Line, Select, point Lock, line Length, two-line Angle, profile height, timeline, dimension-label, and keyboard controls. It does not dispatch Angle or Extrude through a helper. The two-line selection naturally exposes the Angle card; typing there avoids a second ribbon press that would submit the already-visible card value. The selection order is line `4` then line `6`, and the native constraint row preserves that directed order.
|
||||
|
||||
The full saved 30-degree constraint array is exactly Horizontal(line 4), Lock(point 2 at the origin), Distance(2,3,10), Distance(3,5,10), and Angle(4,6,pi/6). The edited array changes only the Angle value to pi/3. These five scalar constraints remove all six planar degrees of freedom of the three points together with the Horizontal row, matching the script's no-warning checks.
|
||||
|
||||
For adjacent edge lengths `a=b=10`, triangle area is `a*b*sin(theta)/2`. Extruding height 2 therefore gives volume `100*sin(theta)`:
|
||||
|
||||
- 30 degrees: `50 mm³`;
|
||||
- 60 degrees: `50*sqrt(3) = 86.60254037844386 mm³`.
|
||||
|
||||
All four STEP assertions require five faces, one solid, and relative tolerance `1e-9`. `/tmp/vernier-natural-dim-sketch-angle/report.json` passes 116 steps and 251 frames, deterministic across process IDs `[14,73]` on llvmpipe. `angle30.step` is byte-identical to `angle-undo.step` (SHA-256 `43519ac3…`), and `angle60.step` is byte-identical to `angle-redo.step` (SHA-256 `326125a7…`), with the same pairings across both processes.
|
||||
|
||||
The false oracle `/tmp/vernier-sketch-angle-wrong.json` differs from the committed script only at step 99, changing the edited volume from `86.60254037844386` to `87.60254037844386`. `/tmp/vernier-natural-dim-angle-wrong/report.json` exits 1 at that exact STEP assertion after the initial volume and edited native constraint pass; actual volume is `86.60254037849425` and relative error is `1.142e-2 > 1e-9`.
|
||||
|
||||
## Point Distance and Measure workflow
|
||||
|
||||
`scripts/drive/functions-point-distance-measure.json` uses the real Sketch, Line, Select, point Lock, two-point Distance, Inspect tab, `$` command-bar button, Measure command, dimension label, value card, and Ctrl+Z/Y routes. The report traces show the first measurement produces `distance 20.000 mm`, label editing produces `distance 16.000 mm`, Undo restores 20, and Redo restores 16. The feature count remains one and every measured state has no warning.
|
||||
|
||||
The full native arrays are exactly Horizontal(line 4), Lock(point 2 at the origin), and Distance(2,3,20 or 16). The retained files independently confirm that authored point 3 stays at 10 mm. Native `distance20.vernier` equals `distance-undo.vernier` byte for byte (SHA-256 `b314a288…`), and `distance16.vernier` equals `distance-redo.vernier` (SHA-256 `9f979ea5…`), with matching artifacts across both processes.
|
||||
|
||||
`/tmp/vernier-natural-dim-point-distance/report.json` passes 131 steps and 305 frames, deterministic across process IDs `[133,192]` on llvmpipe. The false oracle `/tmp/vernier-point-distance-wrong.json` differs only at step 80, changing expected readout `distance 16.000 mm` to `distance 16.001 mm`. `/tmp/vernier-natural-dim-point-wrong/report.json` exits 1 at that exact readout assertion with actual `distance 16.000 mm`.
|
||||
|
||||
## Boundaries kept explicit
|
||||
|
||||
The positive workflows edit an existing constraint by clicking its painted number. They do not show that repeating the natural Length, Distance, or Angle creation action edits that row. The three separate retained red reports each reach the original accepted constraint and then refuse the conflicting duplicate attempt without changing the document:
|
||||
|
||||
- Length: `/tmp/vernier-existing-length-f56-red/report.json`, step 57;
|
||||
- Distance: `/tmp/vernier-existing-point-distance-red-actual/report.json`, step 57;
|
||||
- Angle: `/tmp/vernier-existing-angle-red-direct-card/report.json`, step 92.
|
||||
|
||||
The Measure workflow clicks the command-bar `$` button. It gives no Ctrl+K evidence, consistent with the known driver modifier-event limitation. Reverse Angle pick order, repeated-creation repair, and native worker isolation are outside these two positive gates.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Sketch dimension controls — 2026-09-08
|
||||
|
||||
Frozen baseline: commit `f56bb172dc258f85188edf302a81c8a7397c4602`, executable `/tmp/vernier-drive-f56bb17`, SHA256 `5207fc230e7d463cc810240451387c2a8ddf37b543ad0ed7e0bc73add22e2e7e`. Root owns these scripts, registrations and evidence. No production repair of the repeated-creation routes is claimed here.
|
||||
|
||||
## Angle creation and existing number editing
|
||||
|
||||
`scripts/drive/functions-sketch-angle.json` starts empty, draws a triangular chain, locks its origin, and gives its first two directed edges length10 through their actual cards. It selects those two edges in order and types30 in their already visible Angle card. The saved full constraint array must contain exactly Horizontal(line4), Lock(point2), Distance(2,3,10), Distance(3,5,10), and Angle(4,6,pi/6).
|
||||
|
||||
The actual height card extrudes by2. The triangle area is `a*b*sin(theta)/2`, where theta is the directed turn between consecutive edges; extrusion volume is therefore `100*sin(theta)`. At30 degrees this is50mm³. After selecting the sketch, the script clicks the painted `30.0°` label and types60 in its value card. The same stored Angle row becomes pi/3, verified in native JSON. The existing extrusion then has volume `50*sqrt(3) = 86.60254037844386 mm³`. Actual Ctrl+Z/Y restore50/86.60254037844386. All four STEP assertions require five faces, one solid and relative volume tolerance1e-9, with no compile warnings. Original30-degree STEP bytes equal Undo; edited60-degree bytes equal Redo.
|
||||
|
||||
`/tmp/vernier-natural-dim-sketch-angle/report.json` passes **116 steps /251 frames**, deterministic across separate processes on llvmpipe. Every exported artifact also matches between the two runs. `/tmp/vernier-natural-dim-angle-wrong/report.json` changes only the edited volume by+1 and fails at step99 with exit1. It reaches and passes the native-angle and initial-volume assertions first.
|
||||
|
||||
The Angle card is already visible for a two-line selection. An initial authoring probe additionally clicked its already visible ribbon action, which executed the current90-degree default before the subsequent typed30. That probe was not evidence that first Angle creation fails. The final positive and proper repeated-creation probes type directly into the visible card.
|
||||
|
||||
## Two-point distance and solved-point Measure
|
||||
|
||||
`scripts/drive/functions-point-distance-measure.json` draws a horizontal line whose authored endpoints are0 and10mm, locks the first point, selects both points and types Distance20. It presses the actual command-bar button, chooses Measure, and requires `distance 20.000 mm`. The authored second point is still10 in native JSON: this assertion distinguishes a measurement of solved geometry from a read of authored points.
|
||||
|
||||
The script selects the painted20.00 dimension label, types16 in its value card and asserts the full native constraint array. Actual point picks and command-bar Measure then require16mm. Ctrl+Z/Y restore20/16mm, each checked by the exact stored Distance and a fresh real Measure request. The sketch must retain one feature and no warnings after every measurement. Native document bytes for20 equal Undo; bytes for16 equal Redo.
|
||||
|
||||
`/tmp/vernier-natural-dim-point-distance/report.json` passes **131 steps /305 frames**, deterministic across separate processes on llvmpipe, with all artifacts matching. `/tmp/vernier-natural-dim-point-wrong/report.json` changes only the first edited measurement expectation to16.001mm and fails at step80 with exit1. This open sketch has no solid to export; the native constraints and solved-point measurement are its document/geometry oracles. Measurement precision is the displayed0.001mm, not a claim of a higher precision mesh oracle.
|
||||
|
||||
Save and STEP helpers in these scripts are explicitly oracles, not file-control coverage. Both workflows pin llvmpipe and skip PNG comparisons. They are registered in the check script; Angle joins the modeling-workflow test, and point measurement has a separate test because there is no solid to export. They must be rerun with the pending profile repair before a combined checkpoint. These bounded harness runs do not implement native process hang containment.
|
||||
|
||||
## Repeated natural dimension creation remains broken
|
||||
|
||||
All three creation routes append a fresh constraint rather than locating the one already attached to the picked geometry. The existing dimension-label edit route works; these are distinct controls.
|
||||
|
||||
| Natural route | Frozen baseline evidence |
|
||||
| --- | --- |
|
||||
| Select the previously dimensioned Polygon edge and type Length24 instead of20 | `/tmp/vernier-existing-length-f56-red/report.json`, exit1 at step57 after the constraint conflict. Picked curve6 has original Distance(3,4,20). The retained terminal log is `/tmp/vernier-existing-length-f56-red.log`. |
|
||||
| Select the same two points and type Distance16 after20 | `/tmp/vernier-existing-point-distance-red-actual/report.json`, exit1 at step57. Actual Measure20 already passed; the failure bundle retains the original Distance(2,3,20). |
|
||||
| Select the same ordered two edges and type Angle60 after30 | `/tmp/vernier-existing-angle-red-direct-card/report.json`, exit1 at step92. The original30-degree triangle extrusion already passed its50mm³/five-face oracle. The failure bundle retains the original ordered Angle(4,6,pi/6). |
|
||||
|
||||
Current baseline scripts are preserved as `/tmp/vernier-natural-{length,distance,angle}-red.json`. The source diagnosis is `server/apply_constraints.rs`: ConstrainLength, ConstrainPoints::Distance and ConstrainAngle each construct AddSketchGeometry. Only the separate SetSketchDimension route carries an existing constraint token. The registry deliberately marks these creation cards Seed::Last; therefore the final parameter-preservation repair did not convert them into existing-dimension editors.
|
||||
|
||||
Two authoring confounds are excluded. `ribbon:measure` is ambiguous with its Inspect group caption, and the driver does not emit the modifier-change event needed for Ctrl+K (`drive/run.rs` records this limitation). In the failed shortcut probe the command bar stayed closed and Enter reached another visible card. The corrected point-distance probe clicks `$`, then types Measure, and passes the intended measurement before reproducing the Distance conflict. No Ctrl+K or direct Measure-ribbon coverage is credited.
|
||||
|
||||
## Review correction after SSD resume
|
||||
|
||||
The point-distance script now explicitly asserts authored point3 `[10.0,-0.0]` in the saved Distance20 document before measuring the solved20mm distance. Its notes describe the actual accumulated point selection without claiming the second click uses Ctrl. The new assertion increases this script to132 steps; the earlier131-step reports and step80 false oracle are historical. A fresh combined run and shifted false oracle are required for the revised script.
|
||||
|
||||
## Fresh combined acceptance
|
||||
|
||||
On release SHA256 `3ad9df4c2b7c2d81a17c6fb92dc8467a554db19f50e9bf7b3fb0e7f6e76ab75c`, `/tmp/vernier-profile-resumed-frozen-functions-sketch-angle/report.json` passes116steps/251frames and `/tmp/vernier-profile-resumed-frozen-functions-point-distance-measure/report.json` passes132steps/305frames, both deterministic in separate processes on llvmpipe. Full artifact byte comparison passes between processes. Angle original/Undo and edited/Redo STEP pairs, and point-Distance original/Undo and edited/Redo native pairs, also match byte-for-byte.
|
||||
|
||||
Fresh one-field negative controls are `/tmp/vernier-profile-resumed-functions-sketch-angle-wrong-volume/report.json` (exit1 step99), `/tmp/vernier-profile-resumed-point-wrong-readout/report.json` (exit1 step81) and `/tmp/vernier-profile-resumed-point-wrong-authored/report.json` (exit1 step38). The last proves the new authored10mm assertion observes the saved point and rejects11mm. These supersede the original point script's131-step acceptance for the revised script only; the historical repeated-creation failures remain unfixed.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Natural Length, Distance and Angle drive plan — 2026-09-08
|
||||
|
||||
Status: **prepared, not run, not repair evidence**. This freezes the next control/evidence pass without changing production code or the existing positive scripts. The active checkpoint proves the separate selected-profile/Cut work only.
|
||||
|
||||
## Historical red evidence retained
|
||||
|
||||
The frozen `f56bb172dc258f85188edf302a81c8a7397c4602` binary and reports are historical evidence that the three natural repeat routes append a conflicting constraint. The JSON runner reports zero-based indices:
|
||||
|
||||
| Route | Preserved input | Historical report | Expected failure |
|
||||
| --- | --- | --- | --- |
|
||||
| Length | `/tmp/vernier-natural-length-red.json` | `/tmp/vernier-existing-length-f56-red/report.json` | `failed_step: 57`, `expect_no_error`, after natural Length 20→24 |
|
||||
| Distance | `/tmp/vernier-natural-distance-red.json` | `/tmp/vernier-existing-point-distance-red-actual/report.json` | `failed_step: 57`, `expect_no_error`, after natural Distance 20→16 |
|
||||
| Angle | `/tmp/vernier-natural-angle-red.json` | `/tmp/vernier-existing-angle-red-direct-card/report.json` | `failed_step: 92`, `expect_no_error`, after natural Angle 30→60 |
|
||||
|
||||
Those reports do not verify a future build. The old executable is gone. Preserve these three inputs and reports unchanged so the repaired binary can be compared against the exact failed controls.
|
||||
|
||||
## Frozen interaction and angle policy
|
||||
|
||||
Length matches the selected line's exact endpoint identities. Distance matches exactly the selected point set without regard to pick order, but editing keeps the stored row's original `a,b` references. Angle matches directed line identities. All matches stay inside one sketch and require exactly one complete authored row.
|
||||
|
||||
For Angle, the stored row's references and orientation are canonical. Selecting them in reverse presents the exact unary negation. A reverse-card edit is negated back before exact comparison and storage, so editing stored `(a,b,+30°)` through `(b,a)` with `-60°` retains `(a,b)` and stores `+60°`. A transformed candidate equal to the stored `f64` is a no-op. Values separated by `2π` are different authored values and must not be treated as equivalent.
|
||||
|
||||
New Angle creation accepts finite degrees in `[-180,180]`; the antiparallel tie is `+180°`. An existing finite target bypasses that creation range. Exact `270°` and `450°` targets remain `3π/2` and `5π/2` rather than being clamped or normalized. Reversing stored `+π` presents exact `-π` and an untouched reverse submission maps back to exact `+π`.
|
||||
|
||||
## Prepared driven inputs
|
||||
|
||||
Draft inputs live outside the repository so they cannot enter the current gate accidentally:
|
||||
|
||||
- `/tmp/vernier-dimension-controls/natural-length-create-or-edit.json`
|
||||
- `/tmp/vernier-dimension-controls/natural-distance-create-or-edit.json`
|
||||
- `/tmp/vernier-dimension-controls/natural-angle-create-or-edit.json`
|
||||
|
||||
They are valid JSON drafts only; no current binary has parsed or executed them. Before first execution, compare each prefix with its retained red input and keep the original natural picks/cards. Do not substitute the working painted-number edit route.
|
||||
|
||||
The Length draft keeps the original polygon row at `constraints[3] = Distance(3,4,…)`, asserts the full initial and edited arrays, analytic equilateral-prism volumes, normal Undo/Redo, a non-round `23.123456789` target, and redo witnesses around untouched and exactly equivalent Enter.
|
||||
|
||||
That polygon remains free to translate. Its rigid-motion degrees of freedom do not change the analytic volume, so the draft deliberately makes no `expect_no_warnings` claim. A later instrument pass may assert the exact underconstrained warning only after reading it from the repaired run; it must not add unrelated locks merely to make the natural-dimension probe quiet.
|
||||
|
||||
The Distance draft keeps `constraints[2] = Distance(2,3,…)`, asserts the complete array and authored point 3, measures solved point spacing through the actual Inspect/Measure control, exercises normal Undo/Redo, uses non-round `18.123456789`, gives untouched/equivalent redo witnesses, and finally selects point 3 before point 2 while requiring the stored row to remain `(2,3)`.
|
||||
|
||||
The Angle draft keeps `constraints[4] = Angle(4,6,…)`, asserts analytic triangle-prism volumes, normal Undo/Redo, exact non-round degree/radian values, reverse-selection untouched/equivalent redo witnesses, and reverse edits that retain `(4,6)` while storing exact nonprincipal `270°` and `450°` targets.
|
||||
|
||||
The first instrument pass must treat coordinate targets in these drafts as provisional. Solved geometry moves after each dimension edit, and a world click is only evidence if the trace reports the intended `line`/`two-lines`/`two-points` selection immediately before the card. Retarget a click only from the failure bundle and record the measured reason.
|
||||
|
||||
## Required app-level controls
|
||||
|
||||
The JSON driver cannot inspect `Document::to_session_json()`, pending job count, dirty state or allocator state, and it cannot compare two differently named artifacts within one run. Therefore redo survival in the draft scripts is a user-control witness, not proof of complete no-op identity. Add bounded actual-shell/app controls that:
|
||||
|
||||
1. build a redo witness, snapshot exact worker-owned session JSON, dirty state, allocator and submitted-job count, then use the real selection/card/Enter path;
|
||||
2. prove untouched and explicitly typed equivalent Length, Distance and forward/reverse Angle submit no job, preserve every snapshot bit, and leave actual Ctrl+Y effective;
|
||||
3. prove a changed value submits once and one Undo restores the entire prior payload;
|
||||
4. prove new untouched geometry-valued Length, Distance and Angle creation still commits one row and changes the expected degrees of freedom;
|
||||
5. prove solved seed availability explicitly. `SketchPointRow.at` may fall back to authored coordinates, so a missing solved position must yield unavailable/refusal rather than being advertised as solved;
|
||||
6. prove source completeness explicitly. `SceneView.sketch_dimensions` is a filtered annotation list and can omit a live authored constraint when its frame or anchor cannot be drawn; it cannot establish absence. The natural matcher needs complete immutable authored metadata or an explicit unavailable state;
|
||||
7. prove worker revalidation of the expected match. For Length this includes selected line endpoints changing while an old Distance row survives. For all routes, a duplicate added after card preparation, a removed row, changed kind/references, foreign sketch, or missing entity must refuse without mutation, panic or history allocation;
|
||||
8. treat the current semantic token as `(sketch, index, full constraint)`. An identical remove-and-reinsert at the same slot is deliberately indistinguishable because the document has no persistent constraint-row identity; do not invent a format or revision field for this repair.
|
||||
|
||||
Also cover dirty same-kind A→B switching and an abandoned partial draft, and verify that a successful acknowledgement refreshes the exact source. Existing label editing, point Measure and Angle workflows stay as controls and must remain green unchanged.
|
||||
|
||||
For the nonprincipal Angle boundary, app controls must also prove untouched existing `270°` and `450°` bypass the creation clamp, and that editing stored `450°` to `90°` is one real edit even though both targets drive modulo-equivalent geometry. This pins exact authored-value comparison rather than geometry-only equality.
|
||||
|
||||
## Creation seeds and negative controls
|
||||
|
||||
Lower-layer/app fixtures must pin the initial proposals from solved geometry before any dimension exists:
|
||||
|
||||
- line Length and point Distance use current solved positions, including a fixture where authored and solved points differ;
|
||||
- directed Angle uses `atan2(cross,dot)` in `[-π,π]`, with clockwise geometry negative and antiparallel exactly `+π`;
|
||||
- pressing untouched Enter on those creation cards appends exactly one row even though the typed value equals current geometry;
|
||||
- existing authored values always beat recomputed solver approximations.
|
||||
|
||||
Zero matches against complete live authored metadata is the creation case, with a solved geometric seed. The worker must recheck the expected absence; if a matching row appeared after preparation, that stale creation request refuses instead of appending a duplicate or turning itself into an edit. Required refusals are more than one exact match, incomplete/unavailable source, a stale existing-edit token, stale expected absence, missing sketch/point/curve, and a non-finite typed value. Add false controls that perturb one native row value and one geometry/readout oracle so a green run cannot be explained by an assertion that never observed the repair.
|
||||
|
||||
## Execution order after the writer hands off a fixed binary
|
||||
|
||||
1. Parse and single-run the three draft scripts with llvmpipe, `--once --skip-png --require-adapter llvmpipe`. Fix only measured input-coordinate or target-text ambiguity.
|
||||
2. Run each cross-process and require distinct PIDs, deterministic frames/traces, and matching artifacts between processes.
|
||||
3. Add one false native-value or wrong-geometry derivative per route and require exit 1 at its own changed assertion after the repaired natural edit has passed.
|
||||
4. Run the app-level session/worker tests above, the unchanged positive Angle and point-Measure scripts, parameter-card gates, profile-reuse gates, Cut, Section, selftest and the full workspace gate.
|
||||
5. Record executable path, commit, SHA-256, exact commands, reports, failure indices and artifact digests in a verification document. Until then, describe these files only as prepared controls.
|
||||
|
||||
Do not register or commit the `/tmp` drafts until they have passed independent review and the combined gate. Registration belongs to the repair owner after the controls are green.
|
||||
@@ -0,0 +1,136 @@
|
||||
# Natural dimension repair plan review — 2026-09-08
|
||||
|
||||
Read-only planning review on reliability checkpoint `f56bb17` plus the selected
|
||||
profile/Cut repair. No natural-dimension production change, Cargo run or commit
|
||||
was made by this reviewer. This report is separate from the completed Cut
|
||||
regression review. Production must wait for the parent-owned combined checkpoint.
|
||||
|
||||
## Plan assessment
|
||||
|
||||
The updated `NATURAL_DIMENSION_REPAIR_PLAN_2026-09-08.md` defines a coherent
|
||||
create-or-edit contract for the natural Length, two-point Distance and ordered
|
||||
two-line Angle cards. Its signed-angle bullet now resolves the policy that was
|
||||
left open in the earlier draft. No design blocker is identified in that revised
|
||||
policy. This is a plan assessment, not verification that the behavior exists.
|
||||
|
||||
The baseline source confirms the diagnosed split: the three natural routes in
|
||||
`server/apply_constraints.rs` append `AddSketchGeometry`, while
|
||||
`SetSketchDimension` edits a row using its complete expected constraint and index
|
||||
hint. That token already preserves original references and refuses a replaced
|
||||
row. It does not by itself express an expected absence or prove that an old
|
||||
Length constraint still belongs to a selected line after its endpoints change.
|
||||
|
||||
Here, replacement means an observable token change: different sketch, index,
|
||||
constraint kind, references or value. Constraints have no persistent allocated
|
||||
identity of their own. Removing and reinserting an identical row in the same
|
||||
slot is indistinguishable under the existing `(sketch, index, full constraint)`
|
||||
contract; this review does not require a new hidden identity, revision counter,
|
||||
document format or dependency to distinguish that history. The exact token proves
|
||||
the semantic target. Existing draft logic may separately ignore numeric source
|
||||
changes for ownership while still refreshing the full expected worker token.
|
||||
|
||||
## Acceptance details the implementation must retain
|
||||
|
||||
1. **Signed Angle representation.** Match either order of the same pair, preserve
|
||||
the stored row's original references, and use exact unary negation for reverse
|
||||
selection. A stored 270-degree-equivalent target displays -270 on reverse
|
||||
selection, not +90; 450 reverses to -450. Existing +π reverses to -π. Existing
|
||||
editing accepts finite targets outside the new-creation range without implicit
|
||||
wrapping or clamping. New creation alone uses [-180,180] degrees; antiparallel
|
||||
solved geometry may deterministically seed +180.
|
||||
2. **Canonical no-op comparison.** Preserve the exact stored radians for an
|
||||
untouched existing field. For edited reverse text, convert once to canonical
|
||||
radians and negate back into stored orientation before equality/dispatch. Do
|
||||
not compare modulo 2π, against formatted text, or against solver-derived
|
||||
geometry. Test both an exactly equal transformed candidate (no edit) and a
|
||||
geometrically equivalent but numerically different target such as 450 → 90
|
||||
(one real edit). A rounded degree display is not evidence of exact equivalence.
|
||||
Worker-side no-op detection must first admit the expected semantic target, then
|
||||
compare the canonical candidate to that current target before executing a
|
||||
command. Equality to a stale expected value must not mask a failed token check.
|
||||
3. **Unique match on both boundaries.** UI and worker need the same distinction
|
||||
between one existing match, verified absence, ambiguity and invalid geometry.
|
||||
The worker must revalidate uniqueness as well as the existing exact token;
|
||||
adding a duplicate after a frame was drawn must not allow an edit to whichever
|
||||
row was previously found first. Duplicate Angle matches in opposite stored
|
||||
orders are still ambiguous. Distance matching is unordered, but writes preserve
|
||||
the row's original point order.
|
||||
4. **Stale creation and geometry.** A natural creation request carries an explicit
|
||||
expected absence for its selected subject. A new matching row appearing before
|
||||
admission refuses before allocation/history; it neither appends a duplicate
|
||||
nor silently becomes an existing edit. For Length, changing the selected line's
|
||||
endpoints while its old Distance row remains must likewise refuse. Check
|
||||
sketch, curve, defining-point liveness and the exact matched relationship before
|
||||
invoking the existing low-level command. Generic AddSketchGeometry behavior is
|
||||
not broadened into an implicit deduplication policy.
|
||||
5. **Draft and source ownership.** Natural cards must derive existing/create mode
|
||||
from explicit match state, not Seed metadata or whether they were rearmed.
|
||||
Source refresh after Undo changes a clean field but preserves appropriately
|
||||
owned dirty text. A different constraint identity, reversed Angle orientation,
|
||||
removed/replaced slot, or different geometric subject must have an explicit
|
||||
draft outcome. Existing exact targets outrank solved measurements; new creation
|
||||
seeds from solved points that demonstrably differ from authored coordinates.
|
||||
An untouched new geometric seed still commits a constraint and removes degrees
|
||||
of freedom.
|
||||
|
||||
The current `parse_field` applies registry clamping before conversion. Simply
|
||||
changing the Angle registry range to [-180,180] would therefore still destroy an
|
||||
edited existing 270/450 target; the parser's creation/existing policy must be
|
||||
separated. The current action dispatch also converts `sketch_angle_deg` to radians,
|
||||
so the implementation must ensure the exact candidate is not converted through
|
||||
display degrees again before reaching the worker.
|
||||
|
||||
One scene-data distinction is material to matching. `sketch_dimensions()` in
|
||||
`server/view.rs:382` builds annotation rows from compiled sketches and skips
|
||||
constraints whose frame or dimension anchor cannot be produced. An absent label
|
||||
row is therefore not proof that the authored document contains no matching
|
||||
constraint. The implementation needs complete immutable authored constraint
|
||||
metadata for its match, or an explicit unavailable state that refuses; it cannot
|
||||
interpret an omitted annotation as expected absence. Likewise,
|
||||
`sketch_points():218` falls back to authored coordinates when no solved point is
|
||||
available. A card claiming a solved initial measurement must distinguish that
|
||||
fallback from actual solved geometry. These completeness distinctions must be
|
||||
made concrete in the source design before claiming the plan is implemented.
|
||||
|
||||
## Evidence allocation
|
||||
|
||||
The next driven controls must preserve the real picks and natural cards from all
|
||||
three retained reds, then assert exact native constraint arrays and independent
|
||||
geometry/readouts with actual Undo/Redo. Label editing remains a separate working
|
||||
route and must not substitute for repeated natural editing. Existing profile,
|
||||
Cut/Boss and Section controls guard shared parameter-card behavior.
|
||||
|
||||
The present driver can demonstrate that an original redo still succeeds, but that
|
||||
alone does not prove byte-identical session state or absence of allocation/dirty
|
||||
changes. Complete `to_session_json`, allocator, history/redo and dirty-state
|
||||
assertions belong in bounded app-level control tests. The corresponding driven
|
||||
redo is a user-visible witness. Expected-absence races, stale line endpoints and
|
||||
duplicate match cases also need worker/app fixtures; their absence from a normal
|
||||
GUI authoring route is not permission to omit boundary tests.
|
||||
|
||||
## Concrete drive-plan review and disposition
|
||||
|
||||
The published `NATURAL_DIMENSION_DRIVE_PLAN_2026-09-08.md` and its three
|
||||
`/tmp/vernier-dimension-controls/natural-{length,distance,angle}-create-or-edit.json`
|
||||
drafts were inspected. They preserve natural picks/cards, original constraint
|
||||
references, non-round targets, and actual Undo/Redo witnesses. Length uses the
|
||||
equilateral-prism formula `sqrt(3)*s²/2` at height 2; Angle uses `100*sin(theta)`
|
||||
for two 10 mm sides at height 2. Distance uses solved Measure with an explicit
|
||||
authored-point assertion. Reverse Angle edits into 270/450 retain `(4,6)` and
|
||||
assert exact stored canonical values. Untouched existing 270/450 and the distinct
|
||||
450 → 90 edit are explicitly assigned to app controls.
|
||||
|
||||
Two review findings were corrected before freeze. The Length draft no longer
|
||||
requires `expect_no_warnings` for a polygon that remains free to translate; its
|
||||
analytic volume does not require a fully constrained position. The drive plan's
|
||||
initial refusal list incorrectly included complete zero matches; it now states
|
||||
that complete zero matches are creation, with a solved seed and expected-absence
|
||||
recheck, while stale/ambiguous/unavailable cases refuse.
|
||||
|
||||
**Disposition: no remaining blocker found in the repair brief and drive-plan
|
||||
design.** The review is frozen for the next implementation lane. The inputs are
|
||||
JSON drafts only, not run or registered, and their coordinates must be calibrated
|
||||
through actual failure bundles if needed. This review does not claim successful
|
||||
natural-dimension controls, executable-schema validation, complete session-state
|
||||
preservation, or implementation completion. The source and evidence requirements
|
||||
above remain acceptance work for that next lane.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Natural dimension edit repair — 2026-09-08
|
||||
|
||||
Status: concrete next repair brief, backed by `NATURAL_DIMENSION_CONTROL_VERIFICATION_2026-09-08.md`; not implemented. Keep the active selected-profile lane isolated. This extends the full function-verification goal rather than declaring the working number-label alternative sufficient.
|
||||
|
||||
## Intended behavior
|
||||
|
||||
Picking an edge, point pair or ordered line pair that already has its dimension must expose that exact existing value in the natural Length, Distance or Angle card. Typing a different value changes that existing constraint once. Untouched/equivalent Enter submits no document edit, preserves exact canonical value and complete history/redo/allocator/dirty state, and does not add a redundant constraint. The explicit dimension-label route remains valid.
|
||||
|
||||
When the selection has no matching dimension, Enter still creates one, even when its value equals the current geometry: adding a constraint removes degrees of freedom and is not a no-op. Prefer the current solved geometric measurement as the new dimension's initial value, not an unrelated last-used length from another subject. Exact authored constraint targets take precedence over solver approximations whenever a matching dimension exists.
|
||||
|
||||
## Matching and command boundary
|
||||
|
||||
- Length targets the selected line's exact endpoint identities. Distance targets exactly the two selected points. Point-pair order is irrelevant for Euclidean Distance; the stored row's original reference order should remain unchanged.
|
||||
- Angle targets directed line vectors. The solver uses atan2(cross,dot) and signed radians. Match either pick order to the same single existing constraint, retain its original stored references, and negate its exact canonical value for reversed picks. Convert an edited reversed value back by negation before updating that row. New creation accepts signed degrees in[-180,180] and seeds from solved directed geometry; a new antiparallel seed may deterministically use+180. Existing constraints use the full finite degree domain, including270/450-degree targets, and bypass creation clamping. Existing+pi reverses to-pi exactly; never wrap or normalize existing targets. No-op compares the transformed canonical candidate to the stored target, not modulo2pi geometric equivalence. Untouched fields preserve stored bits and changed values must remain finite after conversion.
|
||||
- Match within exactly one sketch and require a single unambiguous live constraint. Missing/foreign geometry, duplicate matches or a replaced constraint must refuse truthfully. Do not silently choose the first row from an ambiguous set, merge unrelated constraints or delete redundant rows as a side effect.
|
||||
- Use immutable scene rows for UI ownership/readiness and the existing exact SetSketchDimension token for an existing edit where possible. No Document or solver work on the render thread. Reuse the parameter draft's exact-value, dirty-field and acknowledgement rules; the natural control's subject must include the matched constraint's semantic identity, not just a number or slot index. The existing `(sketch, index, full expected constraint)` token and current selected-geometry checks detect observable kind/reference/value/endpoint replacement; an identical remove-and-reinsert in the same slot is semantically indistinguishable and does not require a new stable ID, revision field or document format.
|
||||
- At the worker boundary, verify the expected row and referenced geometry before mutation. A stale creation request that races with a newly added matching dimension must not append a duplicate or silently retarget another row. Keep low-level generic AddSketchGeometry semantics distinct from the natural card's create-or-edit behavior.
|
||||
- Matching needs a complete immutable authored constraint inventory or an explicit unavailable state. `SceneView.sketch_dimensions` is filtered annotation data (resolved frame and paintable anchor required); absence there does not prove absence of an existing constraint. Add focused metadata at the existing scene boundary rather than reconstructing Document on the UI thread. Likewise, `SketchPointRow.at` can fall back to authored coordinates when solved data is unavailable; carry or check solved-state availability before claiming a new dimension seed is solved geometry. Missing/incomplete source must not silently become creation with a guessed default.
|
||||
- Creation and existing editing need explicit execution policy independent of Seed metadata. Update registry/source coverage accordingly. Preserve canonical radians separately from formatted degrees and preserve exact untouched numeric siblings. No generic Document::execute deduplication or new dependency/native format is needed.
|
||||
|
||||
## Acceptance
|
||||
|
||||
1. Turn all three retained real-control red probes green through the same natural picks/cards, then assert exact constraint count/token/value, analytic solid or solved-point geometry, actual Undo/Redo and unchanged preexisting references.
|
||||
2. For each route, create a redo witness, reselect the exact existing dimension and press untouched Enter, then type an exact equivalent value. Both preserve complete session JSON and allow the original redo. Include non-round f64 values and canonical non-round radians; a geometry tolerance alone cannot prove this.
|
||||
3. Dirty same-kind selection A→B, changed source after Undo, removed/replaced constraint slots, reversed point pairs, reversed angle picks, and ambiguous duplicate constraints must have explicit outcomes. Missing sketch/point/curve requests must refuse without panic or history allocation.
|
||||
4. Unconstrained geometry uses solved positions for the initial proposed dimension; test authored and solved positions that differ. Creating the first constraint with untouched geometry-valued input must commit one row and the expected degrees-of-freedom change. Existing matching rows must never become creation merely because the card was rearmed.
|
||||
5. Keep the working Angle/point-Measure/dimension-label workflows and all parameter-card, profile-reuse, Cut and Section gates green. Add false native-value and wrong-geometry/readout controls. Obtain independent review and complete the combined gate before claiming the repair.
|
||||
|
||||
The broader driver modifier-event gap, geometric-refusal transaction history, arbitrary region selection and native worker isolation are separate requirements. This brief does not claim to solve them by relabelling the natural controls as creation-only.
|
||||
@@ -139,3 +139,9 @@ The active parameter-card repair adds typed source values to the scene boundary.
|
||||
Parameter drafts and `ParameterSubmission` belong to the parent shell and are not document-server checkpoint fields. The current in-process app pairs a parent submission token with every actual worker reply through a FIFO that also includes empty slots for background and file requests. In the future supervisor, associate that token with the full generation/request ID; do not pop a token on an unmatched, stale, restore-handshake or heartbeat response. A failed or never-acknowledged request must not promote creation defaults. Newer partial text or chip choices must survive older valid acknowledgements. Generation failure clears pending acknowledgements together with the other provisional interaction state, while restoring the last acknowledged document checkpoint independently.
|
||||
|
||||
Add a hostile lifecycle test that holds an earlier background result, sends a parameter edit, changes the current draft, then kills/restarts the worker before delivering a stale result. The old result must neither acknowledge the new draft nor promote an uncertain default. Follow with a healthy request in the new generation and prove its document geometry and exact card source. This extends the containment acceptance requirements; neither this plan nor the current FIFO implements native process isolation.
|
||||
|
||||
### Selected-profile scene and draft follow-up
|
||||
|
||||
The selected existing-loop repair adds exact `SketchCurveRow::endpoints: Option<(DocEntityId, DocEntityId)>` and `Selection::ConstructionCircle` (drive spelling `construction-circle`). Preserve endpoints and authored direction in the future scene codec; row order and curve IDs alone do not encode a profile walk. Include circles with no endpoints, line/arc/spline endpoints, and construction circles in the scene fixture and selection inventory.
|
||||
|
||||
The parent-only Extrude parameter subject also includes its canonical resolved selected loop plus structural endpoint/construction metadata. Equivalent pick permutations preserve the same draft; topology or construction changes retire it. This metadata remains in the parent acknowledgement ownership described above, not in DocumentServer session state. No byte codec, child process, or containment is introduced by this repair.
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Profile/Cut regression review — 2026-09-08
|
||||
|
||||
Independent review of the uncommitted selected-profile repair on `f56bb17` in
|
||||
`target/worktrees/reliability`, following the interrupted broad reliability gate.
|
||||
This reviewer owns only this report; production, tests, scripts, AGENTS files and
|
||||
commits remain with their assigned owners. No Cargo command was run concurrently
|
||||
with the repair writer.
|
||||
|
||||
## Confirmed regression and root cause
|
||||
|
||||
The unchanged `scripts/drive/cut-then-boss.json` fails at step 27: the actual
|
||||
Extrude card has no unique `cut` chip. The retained broad log
|
||||
`/tmp/vernier-profile-final-workspace.log:965` and the writer's fresh
|
||||
`/tmp/vernier-profile-cut-red/report.json` agree. The fresh trace proves that
|
||||
drawing produced `profile`, explicitly picking the circle produced `circle`,
|
||||
typing its radius completed, and clicking the Extrude ribbon queued no modelling
|
||||
job. Failure occurs when trying to operate the expected card, before a cut can
|
||||
be created.
|
||||
|
||||
The cause is the parameter-card ownership transition, not circle recognition or
|
||||
the STEP oracle. `subject()` now projects ownership differently for Extrude:
|
||||
its profile is resolved through `profile_for_extrude`, and endpoint/construction
|
||||
metadata appears in `profile_curves`. Ordinary radius editing intentionally keeps
|
||||
its prior identity. Reconciliation previously compared a new owner's clone with
|
||||
only `action` changed to the old action. Those action-specific projections differ
|
||||
for the same picked circle, so reconciliation misclassified radius → Extrude as
|
||||
a changed selection, canceled the explicit arm, and fell back to the radius card.
|
||||
|
||||
The earlier focused test `parameter_creation_default_waits_for_real_success_and_cut_is_one_shot`
|
||||
arms Extrude before creating any radius draft. It therefore exercises successful
|
||||
creation/default acknowledgement without exercising this transition. Passing it
|
||||
could not exclude the driven failure.
|
||||
|
||||
## Reviewed correction and preserved boundaries
|
||||
|
||||
The reviewed correction compares `subject(state, view, draft.subject.action)`
|
||||
with the existing draft subject before deciding whether to preserve an explicit
|
||||
arm. This compares the same action's projection on both sides. Cancellation still
|
||||
restores the old source and discards the old dirty buffer before the newly aimed
|
||||
card takes its own defaults. A changed selection must still discard the arm.
|
||||
Same-action Extrude structure changes continue to participate in full ownership
|
||||
equality and must still retire the dirty height.
|
||||
|
||||
The selection/readiness/dispatch audit found no additional blocker in this slice:
|
||||
|
||||
- `profile_for_extrude` refuses sessions, picked dimensions and picked points.
|
||||
Nonempty explicit curve picks resolve their entire selected set; an invalid
|
||||
explicit set never falls back to `view.closed_profile` or its first circle.
|
||||
- With no explicit curves, the existing transient drawn-profile route remains.
|
||||
It continues to outrank a stale face pick. A newly drawn circle can therefore
|
||||
classify as Profile, while an explicitly picked ordinary circle keeps its
|
||||
radius card and can aim Extrude through the ribbon.
|
||||
- The selected graph requires unique live rows/picks, one sketch, no construction
|
||||
geometry, and one degree-two connected component. The worker rechecks both the
|
||||
requested ordered walk and the current selected graph before mutation. Full
|
||||
geometric validation stays after solving/projected-coordinate resolution.
|
||||
- `ConstructionCircle` offers radius editing and the construction toggle without
|
||||
Extrude. `card_showing` also filters explicit arms through current readiness,
|
||||
so preserving an arm cannot make an unavailable construction-circle action live.
|
||||
- Extrude subject canonicalization preserves a dirty height across equivalent
|
||||
pick order and source-radius refreshes. Endpoint/construction metadata remains
|
||||
confined to Extrude, preserving existing radius draft behavior.
|
||||
|
||||
## Verification status
|
||||
|
||||
The writer froze production after the correction. This reviewer inspected the
|
||||
exact corrected branch in `parameter_card.rs:381`, the added
|
||||
`parameter_circle_radius_can_aim_extrude_without_losing_its_subject` test, and the
|
||||
writer's retained logs. The new test creates the preexisting radius draft before
|
||||
aiming Extrude, both with and without a transient chain; verifies dirty height and
|
||||
Cut survive a radius refresh; returns to radius with exact source bits; and proves
|
||||
a changed circle or construction change retires the inappropriate arm/draft.
|
||||
|
||||
| Inspected evidence | Result |
|
||||
| --- | --- |
|
||||
| `/tmp/vernier-profile-cut-ui-red.log` | New regression test fails with armed `None` versus required `Some(Extrude)` before the correction. |
|
||||
| `/tmp/vernier-profile-cut-ui-green.log` | All 19 parameter-filtered tests pass after the correction, including the new regression and existing dirty/default/acknowledgement cases. |
|
||||
| `/tmp/vernier-profile-cut-libraries.log` | App 329, document 294 and UI 345 pass; 3 preexisting UI tests are ignored. |
|
||||
|
||||
These commands were run by the writer, not independently rerun by this reviewer.
|
||||
No duplicate Cargo run was needed for the one-branch correction. The reviewer
|
||||
independently inspected source, test assertions and results. The frozen
|
||||
`parameter_card.rs` SHA256 is
|
||||
`4aff0964bc3e79a3c4c41250f0098b62c0ea65385a7e4164f2aefc6699f3ac0f`.
|
||||
`cut-then-boss.json` has an empty diff against HEAD and SHA256
|
||||
`92ec3b9b7364306a2cb411e94cafaaf50fd2acae09546be86a022af26a9a2b68`.
|
||||
|
||||
The writer then ran the real controls against the rebuilt release binary. This
|
||||
reviewer read each aggregate report and independently verified the binary SHA256
|
||||
`3ad9df4c2b7c2d81a17c6fb92dc8467a554db19f50e9bf7b3fb0e7f6e76ab75c`.
|
||||
|
||||
| Driven aggregate report | Observed result |
|
||||
| --- | --- |
|
||||
| `/tmp/vernier-profile-cut-green/report.json` | Pass, 71 steps / 175 frames; cross-process deterministic, two distinct PIDs, llvmpipe. |
|
||||
| `/tmp/vernier-profile-cut-redo-green/report.json` | Pass, 63 steps / 87 frames; cross-process deterministic, two distinct PIDs, llvmpipe. |
|
||||
| `/tmp/vernier-profile-cut-open-green/report.json` | Pass, 77 steps / 121 frames; cross-process deterministic, two distinct PIDs, llvmpipe. |
|
||||
|
||||
The unchanged Cut/Boss script now reaches its real Cut chip and typed height. It
|
||||
asserts one solid and a first STEP volume delta of `-251.32741228718345` mm³
|
||||
(`-π·4²·5`), then explicitly picks and edits a second circle, aims Extrude, and
|
||||
asserts a second delta of `141.3716694115407` mm³ (`π·3²·5`) without selecting Cut
|
||||
again. Both analytic delta tolerances remain `1e-9`. The two selected rectangle
|
||||
controls still produce 400 mm³ and identical native document, naming sidecar and
|
||||
STEP digests before and after actual Undo/Redo; the Open control additionally
|
||||
passes the actual Save/Open route. These are writer-executed driven results,
|
||||
independently inspected here, not separately rerun by the reviewer.
|
||||
|
||||
**Disposition: no remaining blocker found for this selected-profile/Cut
|
||||
regression.** Its prior real-control red now passes with the same script and
|
||||
oracles, and the new focused test detects the ownership-transition defect.
|
||||
|
||||
The prior implementation/review reports are historical evidence for their earlier
|
||||
snapshot; their “no remaining blocker” wording does not override the subsequently
|
||||
observed broad-gate regression. No main integration, dependency-edge approval,
|
||||
complete R7 closure, or complete reliability-goal claim follows from this review.
|
||||
@@ -242,3 +242,12 @@ and cargo run -q -p vernier-drive -- scripts/drive/functions-polygon.json \
|
||||
--out target/drive/functions-polygon --require-adapter RADV
|
||||
and cargo run -q -p vernier-drive -- scripts/drive/parameter-cards-exact.json \
|
||||
--out target/drive/parameter-cards-exact --require-adapter RADV
|
||||
and cargo run -q -p vernier-drive -- scripts/drive/functions-sketch-angle.json \
|
||||
--out target/drive/functions-sketch-angle --require-adapter RADV
|
||||
and cargo run -q -p vernier-drive -- scripts/drive/existing-profile-redo.json \
|
||||
--out target/drive/existing-profile-redo --require-adapter RADV
|
||||
and cargo run -q -p vernier-drive -- scripts/drive/existing-profile-open.json \
|
||||
--out target/drive/existing-profile-open --require-adapter RADV
|
||||
# Open sketch: native constraints plus actual solved-point measurement.
|
||||
and cargo run -q -p vernier-drive -- scripts/drive/functions-point-distance-measure.json \
|
||||
--out target/drive/functions-point-distance-measure --require-adapter RADV
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"name": "existing-line-profile-after-save-open",
|
||||
"document": "empty",
|
||||
"size": [1600, 1000],
|
||||
"camera": { "target": [0, 0, 0], "distance": 80 },
|
||||
"notes": [
|
||||
"Draw an exact 20 x 10 mm rectangle, save it through the actual title-bar control, dirty the document by undoing the rectangle, and reopen it through the actual command bar and discard prompt.",
|
||||
"Reselect the reopened sides in the deliberately non-walk order top, bottom, left, right. Profile recognition and extrusion use the actual painted height card; Save and STEP export are measurement oracles only.",
|
||||
"The 2 mm extrusion is 20*10*2 = 400 mm3. Real Ctrl+Z removes that extrusion and Ctrl+Y restores the same native profile references and STEP geometry."
|
||||
],
|
||||
"steps": [
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "click", "at": "ribbon:sketch" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "click", "at": "text:Rectangle" },
|
||||
{ "step": "click", "at": "world:-10,-5,0" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "click", "at": "world:10,5,0" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 1 },
|
||||
{ "step": "expect_selection", "is": "profile" },
|
||||
|
||||
{ "step": "click", "at": "text:untitled" },
|
||||
{ "step": "type_path", "path": "{out}/rectangle.vernier" },
|
||||
{ "step": "click", "at": "text:save" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_document", "path": "{out}/rectangle.vernier", "pointer": "/feature:0/payload/Sketch/curves", "equals": {
|
||||
"6": { "Line": { "start": 2, "end": 3 } },
|
||||
"7": { "Line": { "start": 3, "end": 4 } },
|
||||
"8": { "Line": { "start": 4, "end": 5 } },
|
||||
"9": { "Line": { "start": 5, "end": 2 } }
|
||||
} },
|
||||
{ "step": "key", "key": "ctrl+z" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_selection", "is": "none" },
|
||||
|
||||
{ "step": "click", "at": "text:$" },
|
||||
{ "step": "frames", "count": 2 },
|
||||
{ "step": "type", "text": "open" },
|
||||
{ "step": "click", "at": "text:open / export path" },
|
||||
{ "step": "key", "key": "ctrl+a" },
|
||||
{ "step": "type_path", "path": "{out}/rectangle.vernier" },
|
||||
{ "step": "key", "key": "Enter" },
|
||||
{ "step": "frames", "count": 2 },
|
||||
{ "step": "click", "at": "text:discard and open" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_readout", "contains": "opened" },
|
||||
{ "step": "expect_feature_count", "is": 1 },
|
||||
{ "step": "expect_selection", "is": "none" },
|
||||
|
||||
{ "step": "click", "at": "timeline:0" },
|
||||
{ "step": "frames", "count": 2 },
|
||||
{ "step": "expect_selection", "is": "feature:sketch" },
|
||||
{ "step": "click", "at": "world:0,5,0" },
|
||||
{ "step": "click", "ctrl": true, "at": "world:0,-5,0" },
|
||||
{ "step": "click", "ctrl": true, "at": "world:-10,0,0" },
|
||||
{ "step": "click", "ctrl": true, "at": "world:10,0,0" },
|
||||
{ "step": "expect_selection", "is": "profile" },
|
||||
|
||||
{ "step": "click", "at": "card:height" },
|
||||
{ "step": "key", "key": "ctrl+a" },
|
||||
{ "step": "type", "text": "2" },
|
||||
{ "step": "key", "key": "Enter" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 2 },
|
||||
{ "step": "save", "path": "{out}/extruded-created.vernier" },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-created.vernier", "pointer": "/feature:1/payload/Extrude/sketch", "equals": 1 },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-created.vernier", "pointer": "/feature:1/payload/Extrude/profile", "equals": [6, 7, 8, 9] },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-created.vernier", "pointer": "/feature:1/payload/Extrude/height", "equals": 2.0, "tol": 1e-12 },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-created.vernier", "pointer": "/feature:1/payload/Extrude/target/NewBody/body", "equals": 11 },
|
||||
{ "step": "export_step", "path": "{out}/extruded-created.step" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_step", "path": "{out}/extruded-created.step", "volume": 400.0, "faces": 6, "solids": 1, "tol": 1e-9 },
|
||||
|
||||
{ "step": "key", "key": "ctrl+z" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 1 },
|
||||
{ "step": "expect_selection", "is": "profile" },
|
||||
{ "step": "key", "key": "ctrl+y" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 2 },
|
||||
{ "step": "expect_selection", "is": "profile" },
|
||||
{ "step": "save", "path": "{out}/extruded-redone.vernier" },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-redone.vernier", "pointer": "/feature:1/payload/Extrude/sketch", "equals": 1 },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-redone.vernier", "pointer": "/feature:1/payload/Extrude/profile", "equals": [6, 7, 8, 9] },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-redone.vernier", "pointer": "/feature:1/payload/Extrude/height", "equals": 2.0, "tol": 1e-12 },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-redone.vernier", "pointer": "/feature:1/payload/Extrude/target/NewBody/body", "equals": 11 },
|
||||
{ "step": "export_step", "path": "{out}/extruded-redone.step" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_step", "path": "{out}/extruded-redone.step", "volume": 400.0, "faces": 6, "solids": 1, "tol": 1e-9 },
|
||||
{ "step": "expect_warning", "contains": "sketch entity#1: 4 free degrees of freedom" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"name": "existing-line-profile-after-undo-redo",
|
||||
"document": "empty",
|
||||
"size": [1600, 1000],
|
||||
"camera": { "target": [0, 0, 0], "distance": 80 },
|
||||
"notes": [
|
||||
"Draw an exact 20 x 10 mm rectangle, undo and redo its creation, then reselect its four existing sides in the deliberately non-walk order top, bottom, left, right.",
|
||||
"Profile recognition and extrusion use the actual painted height card. Save and STEP export are measurement oracles only.",
|
||||
"The 2 mm extrusion is 20*10*2 = 400 mm3. Real Ctrl+Z removes that extrusion and Ctrl+Y restores the same native profile references and STEP geometry."
|
||||
],
|
||||
"steps": [
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "click", "at": "ribbon:sketch" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "click", "at": "text:Rectangle" },
|
||||
{ "step": "click", "at": "world:-10,-5,0" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "click", "at": "world:10,5,0" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 1 },
|
||||
{ "step": "expect_selection", "is": "profile" },
|
||||
|
||||
{ "step": "key", "key": "ctrl+z" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 1 },
|
||||
{ "step": "expect_selection", "is": "none" },
|
||||
{ "step": "key", "key": "ctrl+y" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 1 },
|
||||
{ "step": "expect_selection", "is": "none" },
|
||||
|
||||
{ "step": "click", "at": "timeline:0" },
|
||||
{ "step": "frames", "count": 2 },
|
||||
{ "step": "expect_selection", "is": "feature:sketch" },
|
||||
{ "step": "click", "at": "world:0,5,0" },
|
||||
{ "step": "click", "ctrl": true, "at": "world:0,-5,0" },
|
||||
{ "step": "click", "ctrl": true, "at": "world:-10,0,0" },
|
||||
{ "step": "click", "ctrl": true, "at": "world:10,0,0" },
|
||||
{ "step": "expect_selection", "is": "profile" },
|
||||
|
||||
{ "step": "click", "at": "card:height" },
|
||||
{ "step": "key", "key": "ctrl+a" },
|
||||
{ "step": "type", "text": "2" },
|
||||
{ "step": "key", "key": "Enter" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 2 },
|
||||
{ "step": "save", "path": "{out}/extruded-created.vernier" },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-created.vernier", "pointer": "/feature:1/payload/Extrude/sketch", "equals": 1 },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-created.vernier", "pointer": "/feature:1/payload/Extrude/profile", "equals": [6, 7, 8, 9] },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-created.vernier", "pointer": "/feature:1/payload/Extrude/height", "equals": 2.0, "tol": 1e-12 },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-created.vernier", "pointer": "/feature:1/payload/Extrude/target/NewBody/body", "equals": 11 },
|
||||
{ "step": "export_step", "path": "{out}/extruded-created.step" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_step", "path": "{out}/extruded-created.step", "volume": 400.0, "faces": 6, "solids": 1, "tol": 1e-9 },
|
||||
|
||||
{ "step": "key", "key": "ctrl+z" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 1 },
|
||||
{ "step": "expect_selection", "is": "profile" },
|
||||
{ "step": "key", "key": "ctrl+y" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 2 },
|
||||
{ "step": "expect_selection", "is": "profile" },
|
||||
{ "step": "save", "path": "{out}/extruded-redone.vernier" },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-redone.vernier", "pointer": "/feature:1/payload/Extrude/sketch", "equals": 1 },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-redone.vernier", "pointer": "/feature:1/payload/Extrude/profile", "equals": [6, 7, 8, 9] },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-redone.vernier", "pointer": "/feature:1/payload/Extrude/height", "equals": 2.0, "tol": 1e-12 },
|
||||
{ "step": "expect_document", "path": "{out}/extruded-redone.vernier", "pointer": "/feature:1/payload/Extrude/target/NewBody/body", "equals": 11 },
|
||||
{ "step": "export_step", "path": "{out}/extruded-redone.step" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_step", "path": "{out}/extruded-redone.step", "volume": 400.0, "faces": 6, "solids": 1, "tol": 1e-9 },
|
||||
{ "step": "expect_warning", "contains": "sketch entity#1: 4 free degrees of freedom" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
{
|
||||
"name": "point-distance-and-measure-actual-controls",
|
||||
"document": "empty",
|
||||
"size": [
|
||||
1600,
|
||||
1000
|
||||
],
|
||||
"camera": {
|
||||
"target": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"distance": 80
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "ribbon:sketch"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:Line"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:0,0,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:10,0,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:Select"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:0,0.5,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "point:free"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:▣"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "point:locked"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:10,0.5,0",
|
||||
"ctrl": true
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "two-points"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "ribbon:distance"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:distance"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+a"
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "20"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "save",
|
||||
"path": "{out}/distance20.vernier"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/distance20.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/constraints",
|
||||
"equals": [
|
||||
{
|
||||
"Horizontal": {
|
||||
"line": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"Lock": {
|
||||
"point": 2,
|
||||
"x": -0.0,
|
||||
"y": -0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"Distance": {
|
||||
"a": 2,
|
||||
"b": 3,
|
||||
"distance": 20.0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/distance20.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points/3",
|
||||
"equals": [
|
||||
10.0,
|
||||
-0.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "tab:inspect"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:$"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "measure"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_readout",
|
||||
"is": "distance 20.000 mm"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 1
|
||||
},
|
||||
{
|
||||
"step": "expect_no_warnings"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "tab:sketch"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:20.00"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "dimension:distance"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:value"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+a"
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "16"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "save",
|
||||
"path": "{out}/distance16.vernier"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/distance16.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/constraints",
|
||||
"equals": [
|
||||
{
|
||||
"Horizontal": {
|
||||
"line": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"Lock": {
|
||||
"point": 2,
|
||||
"x": -0.0,
|
||||
"y": -0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"Distance": {
|
||||
"a": 2,
|
||||
"b": 3,
|
||||
"distance": 16.0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "tab:sketch"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:0,0.5,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:16,0.5,0",
|
||||
"ctrl": true
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "two-points"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "tab:inspect"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:$"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "measure"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_readout",
|
||||
"is": "distance 16.000 mm"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 1
|
||||
},
|
||||
{
|
||||
"step": "expect_no_warnings"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+z"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "save",
|
||||
"path": "{out}/distance-undo.vernier"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/distance-undo.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/constraints",
|
||||
"equals": [
|
||||
{
|
||||
"Horizontal": {
|
||||
"line": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"Lock": {
|
||||
"point": 2,
|
||||
"x": -0.0,
|
||||
"y": -0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"Distance": {
|
||||
"a": 2,
|
||||
"b": 3,
|
||||
"distance": 20.0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "tab:sketch"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:0,0.5,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:20,0.5,0",
|
||||
"ctrl": true
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "two-points"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "tab:inspect"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:$"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "measure"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_readout",
|
||||
"is": "distance 20.000 mm"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 1
|
||||
},
|
||||
{
|
||||
"step": "expect_no_warnings"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+y"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "save",
|
||||
"path": "{out}/distance-redo.vernier"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/distance-redo.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/constraints",
|
||||
"equals": [
|
||||
{
|
||||
"Horizontal": {
|
||||
"line": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"Lock": {
|
||||
"point": 2,
|
||||
"x": -0.0,
|
||||
"y": -0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"Distance": {
|
||||
"a": 2,
|
||||
"b": 3,
|
||||
"distance": 16.0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "tab:sketch"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:0,0.5,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:16,0.5,0",
|
||||
"ctrl": true
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "two-points"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "tab:inspect"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:$"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "measure"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_readout",
|
||||
"is": "distance 16.000 mm"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 1
|
||||
},
|
||||
{
|
||||
"step": "expect_no_warnings"
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
"Draw one horizontal line and lock its first point with actual controls. Select both points, type Distance20, and invoke Measure through the actual command bar; the solved distance must be20mm even though authored second point remains at10mm.",
|
||||
"Click the existing20.00 dimension label, type value16, and verify both its exact native constraint row and the actual solved-point Measure16mm. Actual Undo/Redo restore20/16mm respectively.",
|
||||
"Native Save helpers are document oracles, not file-control coverage. This open-sketch workflow has no solid to export; actual measured solved distance and exact constraint arrays are the geometry/document oracles. The same-point-selection Distance creation route still adds a conflicting constraint when repeated and is recorded separately.",
|
||||
"Use the command-bar button: the current driver does not emit modifier-change input needed for Ctrl+K. No Ctrl+K coverage is claimed."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
{
|
||||
"name": "sketch-angle-and-dimension-edit-actual-controls",
|
||||
"document": "empty",
|
||||
"size": [
|
||||
1600,
|
||||
1000
|
||||
],
|
||||
"camera": {
|
||||
"target": [
|
||||
5,
|
||||
5,
|
||||
0
|
||||
],
|
||||
"distance": 80
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "ribbon:sketch"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:Line"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:0,0,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:10,0,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:13,9,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:0,0,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "profile"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:Select"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:0,0.5,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "point:free"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:\u25a3"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:5,0,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "line"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:length"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+a"
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "10"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:11.5,4.5,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "line"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:length"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+a"
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "10"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:7.5,0,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:10.7,2.1,0",
|
||||
"ctrl": true
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "two-lines"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:angle"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+a"
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "30"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "save",
|
||||
"path": "{out}/angle30.vernier"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/angle30.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/constraints",
|
||||
"equals": [
|
||||
{
|
||||
"Horizontal": {
|
||||
"line": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"Lock": {
|
||||
"point": 2,
|
||||
"x": -0.0,
|
||||
"y": -0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"Distance": {
|
||||
"a": 2,
|
||||
"b": 3,
|
||||
"distance": 10.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"Distance": {
|
||||
"a": 3,
|
||||
"b": 5,
|
||||
"distance": 10.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"Angle": {
|
||||
"a": 4,
|
||||
"b": 6,
|
||||
"radians": 0.5235987755982988
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:0,20,0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "profile"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:height"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+a"
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "2"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 2
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/angle30.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/angle30.step",
|
||||
"volume": 50.0,
|
||||
"faces": 5,
|
||||
"solids": 1,
|
||||
"tol": 1e-09
|
||||
},
|
||||
{
|
||||
"step": "expect_no_warnings"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "timeline:0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:30.0\u00b0"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "dimension:angle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:value"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+a"
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "60"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_warnings"
|
||||
},
|
||||
{
|
||||
"step": "save",
|
||||
"path": "{out}/angle60.vernier"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/angle60.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/constraints",
|
||||
"equals": [
|
||||
{
|
||||
"Horizontal": {
|
||||
"line": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"Lock": {
|
||||
"point": 2,
|
||||
"x": -0.0,
|
||||
"y": -0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"Distance": {
|
||||
"a": 2,
|
||||
"b": 3,
|
||||
"distance": 10.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"Distance": {
|
||||
"a": 3,
|
||||
"b": 5,
|
||||
"distance": 10.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"Angle": {
|
||||
"a": 4,
|
||||
"b": 6,
|
||||
"radians": 1.0471975511965976
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 2
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/angle60.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/angle60.step",
|
||||
"volume": 86.60254037844386,
|
||||
"faces": 5,
|
||||
"solids": 1,
|
||||
"tol": 1e-09
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+z"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_warnings"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 2
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/angle-undo.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/angle-undo.step",
|
||||
"volume": 50.0,
|
||||
"faces": 5,
|
||||
"solids": 1,
|
||||
"tol": 1e-09
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+y"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_warnings"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 2
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/angle-redo.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/angle-redo.step",
|
||||
"volume": 86.60254037844386,
|
||||
"faces": 5,
|
||||
"solids": 1,
|
||||
"tol": 1e-09
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
"Draw a triangle, lock its origin, and type10mm on the first two edges. Pick those directed edges in order and type30 degrees into their already visible Angle card. Native constraints must contain exactly one Angle(4,6,pi/6).",
|
||||
"Extrude height2. Volume is10*10*sin(30deg)=50mm3. Select the sketch and click its30.0-degree dimension label; type60 in the value card. Native angle becomespi/3 and volume becomes50*sqrt(3). Actual Undo/Redo restore the two solids.",
|
||||
"All STEP assertions require one solid, five faces and relative volume tolerance1e-9; committed angle states must have no warnings. Save/STEP helpers are document/geometry oracles rather than file-control coverage.",
|
||||
"The Angle card is already visible for the two-line selection. Clicking its already-visible ribbon action would execute its current default before typing; this script types in the card directly. Reusing Angle creation on already dimensioned edges still appends a conflicting row and is recorded in a separate red probe. Directed angle order is preserved; reversing picks is not claimed equivalent."
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user