Fix Trim snapping and verify hole and feature parameter controls
Use camera-scaled Trim capture and a complete shared hover request to keep preview, modifier changes and commits consistent. Suppress popup-covered preview paint in the same frame and preserve foreign point identities. Correct the Pattern Count card overwriting its own count. Add actual Hole, Push/Pull edit, Pattern Count and small-Trim workflows, with analytic STEP or native-topology checks, history and refusal controls. Record the separately reproduced parameter-card value-loss defect and its next repair requirements. Validation: 1411 workspace tests, 24 release headless tests, 44 CLI selftests, six deterministic release cross-process workflows, four intended false-oracle failures, workspace Clippy and formatting. llvmpipe functional evidence; no RADV PNG or native hang-containment claim. Agent: codex-root
This commit is contained in:
@@ -218,7 +218,7 @@ pub(crate) struct VernierApp {
|
||||
/// both together. The armed tool is part of the record because
|
||||
/// switching Trim to Split asks a different question about the same
|
||||
/// pixel.
|
||||
pub(crate) cut_hover_at: Option<(vernier_doc::EntityId, [f64; 2], shell::SketchTool)>,
|
||||
pub(crate) cut_hover_at: Option<cut::CutQuestion>,
|
||||
pub(crate) error: Option<anyhow::Error>,
|
||||
}
|
||||
|
||||
@@ -856,6 +856,7 @@ impl VernierApp {
|
||||
/// safe, and it means the regression test below exercises the SAME
|
||||
/// method the real loop calls rather than a hand-transcribed copy.
|
||||
pub(crate) fn apply_scene(&mut self, scene: Scene) {
|
||||
self.cut_preview_verfallen();
|
||||
self.document_barrier = self.document_barrier.saturating_sub(1);
|
||||
self.pending_jobs = self.pending_jobs.saturating_sub(1);
|
||||
self.compile_error.clone_from(&scene.compile_error);
|
||||
@@ -897,7 +898,11 @@ impl VernierApp {
|
||||
if let Some(probe) = scene.probe {
|
||||
self.probe = Some(probe);
|
||||
}
|
||||
self.last_error.clone_from(&scene.error);
|
||||
// A successful background preview must not erase the refusal from the
|
||||
// user's preceding click. Real compile/worker failures still report.
|
||||
if scene.cut_hover.is_none() || scene.error.is_some() {
|
||||
self.last_error.clone_from(&scene.error);
|
||||
}
|
||||
if let Some(error) = &scene.error {
|
||||
tracing::error!("edit failed: {error}");
|
||||
self.shell.readout = format!("error: {error}");
|
||||
@@ -931,15 +936,17 @@ impl VernierApp {
|
||||
.map(|(_, at)| at);
|
||||
}
|
||||
self.scene = scene.view;
|
||||
// M3 lane B: AN ANSWER TO A QUESTION NOBODY IS ASKING ANY MORE.
|
||||
// A hover round trip outlives the cursor that started it — the
|
||||
// cursor can leave the curve, or the tool can change, while the
|
||||
// compile is in flight — and `hover_at` is the standing question.
|
||||
// Without this the preview would come back and paint itself after
|
||||
// the cursor had already cleared it.
|
||||
if self.hover_at.is_none() {
|
||||
// Preview replies carry their complete question, including tolerance.
|
||||
// Drawing hover is unrelated; matching only its presence used to drop
|
||||
// every real cut answer in Headless and admit stale native answers.
|
||||
if scene.cut_hover.is_none() || scene.cut_hover != self.cut_hover_at {
|
||||
self.scene.cut_preview = None;
|
||||
}
|
||||
if scene.cut_hover.is_none() {
|
||||
// An edit/rebuild replaced the geometry the cached answer used.
|
||||
// Let a stationary cursor ask again after the worker is idle.
|
||||
self.cut_hover_at = None;
|
||||
}
|
||||
// AND THE GLYPH MARKS THE SAME WAY, for a sharper reason: the
|
||||
// server's `hover_proposals` is sticky ON PURPOSE and rides EVERY
|
||||
// scene, so a tool change that stopped the questions would otherwise
|
||||
@@ -1256,6 +1263,7 @@ impl VernierApp {
|
||||
let mut dimension_click = None;
|
||||
let closing = self.close_state != CloseState::Idle || self.retiring_gesture;
|
||||
let mut close_choice = None;
|
||||
let mut pointer_consumed = false;
|
||||
let output = self.egui_ctx.run_ui(raw_input, |ui| {
|
||||
if closing || self.document_barrier != 0 || self.worker_failed {
|
||||
ui.disable();
|
||||
@@ -1326,12 +1334,19 @@ impl VernierApp {
|
||||
});
|
||||
});
|
||||
}
|
||||
// Snapshot pointer ownership after interactive UI has painted,
|
||||
// before the noninteractive cut glyph. A popup can cover a held
|
||||
// cursor without changing its model-space question; suppress
|
||||
// its glyph in this very frame. State retirement and worker
|
||||
// dispatch stay outside the UI closure.
|
||||
pointer_consumed = ui.ctx().egui_wants_pointer_input();
|
||||
// M3 lane B: THE CUT PREVIEW, PROJECTED HERE FOR THE GIZMO'S
|
||||
// REASON — pixels need the live camera and the sketch's frame,
|
||||
// and `vernier_ui` may not learn about either. A preview whose
|
||||
// points do not project (the plane is edge-on) paints nothing
|
||||
// rather than clamping to an edge of the screen.
|
||||
if !closing
|
||||
&& !pointer_consumed
|
||||
&& let (Some(preview), Some(frame)) = (&self.scene.cut_preview, self.sketch_mode)
|
||||
&& deutung_fuer(true, self.shell.tool) == Deutung::Schneiden
|
||||
{
|
||||
@@ -1451,6 +1466,9 @@ impl VernierApp {
|
||||
if let Some(action) = response.action {
|
||||
self.dispatch(action);
|
||||
}
|
||||
if !self.worker_failed && self.document_barrier == 0 {
|
||||
self.refresh_cut_preview(pointer_consumed);
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
@@ -1919,39 +1937,6 @@ impl ApplicationHandler for VernierApp {
|
||||
} else {
|
||||
self.refresh_hover(x, y);
|
||||
}
|
||||
// M3 lane B: THE CUT PREVIEW FOLLOWS THE CURSOR, and only
|
||||
// under a cut tool. It is a read-only round trip — the
|
||||
// sketch's own points decide what the click would mean, and
|
||||
// the render thread has no copy of them (invariant #6) —
|
||||
// coalesced by `hover_step` so at most one is ever in
|
||||
// flight, exactly as the push/pull preview is.
|
||||
if !consumed
|
||||
&& self.close_state == CloseState::Idle
|
||||
&& let Some(frame) = self.sketch_mode
|
||||
&& deutung_fuer(true, self.shell.tool) == Deutung::Schneiden
|
||||
{
|
||||
let at = self.cut_target(x, y, frame);
|
||||
let question = cut::cut_question(at, self.shell.tool);
|
||||
match crate::preview::hover_step(
|
||||
question,
|
||||
self.pending_jobs,
|
||||
self.cut_hover_at,
|
||||
|(curve, near, tool)| Edit::CutHover { curve, near, tool },
|
||||
) {
|
||||
crate::preview::HoverStep::Ask(edit) => {
|
||||
self.cut_hover_at = question;
|
||||
self.submit(edit);
|
||||
}
|
||||
// THE CURSOR LEFT THE CURVE. Dropping the mark is
|
||||
// local and free; waiting for a compile to say so
|
||||
// would leave a cross standing over empty space.
|
||||
crate::preview::HoverStep::Clear => {
|
||||
self.cut_hover_at = None;
|
||||
self.scene.cut_preview = None;
|
||||
}
|
||||
crate::preview::HoverStep::Keep => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, .. } if !consumed => {
|
||||
let lines = match delta {
|
||||
|
||||
@@ -21,6 +21,10 @@ use crate::VernierApp;
|
||||
use crate::edit::Edit;
|
||||
use crate::graphics::viewport_px;
|
||||
|
||||
/// The capture radius belongs to the preview question: Alt or zoom can
|
||||
/// change the answer even when the curve and tool stay the same.
|
||||
pub(crate) type CutQuestion = (DocEntityId, [f64; 2], SketchTool, Option<f64>);
|
||||
|
||||
/// The sketch CURVE nearest `cursor`, point marks ignored.
|
||||
///
|
||||
/// **A CUT CLICK ON A CORNER IS A CUT CLICK, NOT A DROPPED ONE**, and that
|
||||
@@ -72,11 +76,63 @@ pub(crate) fn curve_at(
|
||||
pub(crate) fn cut_question(
|
||||
at: Option<(DocEntityId, [f64; 2])>,
|
||||
tool: SketchTool,
|
||||
) -> Option<(DocEntityId, [f64; 2], SketchTool)> {
|
||||
at.map(|(curve, near)| (curve, near, tool))
|
||||
snap_tolerance_mm: Option<f64>,
|
||||
) -> Option<CutQuestion> {
|
||||
at.map(|(curve, near)| (curve, near, tool, snap_tolerance_mm))
|
||||
}
|
||||
|
||||
impl VernierApp {
|
||||
fn current_cut_question(&self) -> Option<CutQuestion> {
|
||||
let frame = self.sketch_mode?;
|
||||
if !matches!(self.shell.tool, SketchTool::Trim | SketchTool::Split) {
|
||||
return None;
|
||||
}
|
||||
let (x, y) = self.input.cursor?;
|
||||
cut_question(
|
||||
self.cut_target(x, y, frame),
|
||||
self.shell.tool,
|
||||
self.cut_snap_tolerance(x, y, frame),
|
||||
)
|
||||
}
|
||||
|
||||
/// Called by the shared frame loop after pointer ownership is known. A
|
||||
/// stationary modifier/camera change and an idle worker both get a chance
|
||||
/// to refresh; no native-only CursorMoved dispatch is needed.
|
||||
pub(crate) fn refresh_cut_preview(&mut self, pointer_consumed: bool) {
|
||||
let question =
|
||||
if pointer_consumed || self.drag.is_some() || self.input.orbiting || self.input.panning
|
||||
{
|
||||
None
|
||||
} else {
|
||||
self.current_cut_question()
|
||||
};
|
||||
if question != self.cut_hover_at {
|
||||
self.cut_hover_at = None;
|
||||
self.scene.cut_preview = None;
|
||||
}
|
||||
match crate::preview::hover_step(
|
||||
question,
|
||||
self.pending_jobs,
|
||||
self.cut_hover_at,
|
||||
|(curve, near, tool, snap_tolerance_mm)| Edit::CutHover {
|
||||
curve,
|
||||
near,
|
||||
tool,
|
||||
snap_tolerance_mm,
|
||||
},
|
||||
) {
|
||||
crate::preview::HoverStep::Ask(edit) => {
|
||||
self.cut_hover_at = question;
|
||||
self.submit(edit);
|
||||
}
|
||||
crate::preview::HoverStep::Clear => {
|
||||
self.cut_hover_at = None;
|
||||
self.scene.cut_preview = None;
|
||||
}
|
||||
crate::preview::HoverStep::Keep => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// A viewport click under a cut tool: which curve, and where on the
|
||||
/// sketch plane.
|
||||
///
|
||||
@@ -101,7 +157,11 @@ impl VernierApp {
|
||||
// `cut_preview`'s reason: only the two cut tools route here,
|
||||
// and a panic on the render thread is not the way to say the
|
||||
// routing changed.
|
||||
_ => Edit::TrimCurve { curve, near },
|
||||
_ => Edit::TrimCurve {
|
||||
curve,
|
||||
near,
|
||||
snap_tolerance_mm: self.cut_snap_tolerance(cx, cy, frame),
|
||||
},
|
||||
};
|
||||
self.submit(edit);
|
||||
}
|
||||
@@ -123,15 +183,36 @@ impl VernierApp {
|
||||
/// point submit nothing and bring no preview back.
|
||||
pub(crate) fn cut_preview_verfallen(&mut self) {
|
||||
let still_about_something = self.sketch_mode.is_some()
|
||||
&& self.cut_hover_at.map(|(_, _, tool)| tool) == Some(self.shell.tool)
|
||||
&& self.cut_hover_at.map(|(_, _, tool, _)| tool) == Some(self.shell.tool)
|
||||
&& crate::camera::deutung_fuer(true, self.shell.tool)
|
||||
== crate::camera::Deutung::Schneiden;
|
||||
if !still_about_something {
|
||||
let moved = viewport_px(self.graphics.as_ref(), self.offscreen.as_ref()).is_some()
|
||||
&& self.current_cut_question() != self.cut_hover_at;
|
||||
if !still_about_something || moved {
|
||||
self.cut_hover_at = None;
|
||||
self.scene.cut_preview = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Both hover and commit use the drawing gesture's local six-pixel radius.
|
||||
pub(crate) fn cut_snap_tolerance(
|
||||
&self,
|
||||
cx: f64,
|
||||
cy: f64,
|
||||
frame: vernier_doc::SketchFrame,
|
||||
) -> Option<f64> {
|
||||
if self.input.modifiers.alt_key() {
|
||||
return None;
|
||||
}
|
||||
let [width, height] = viewport_px(self.graphics.as_ref(), self.offscreen.as_ref())?;
|
||||
crate::camera::draw_snap_tolerance_mm(
|
||||
&self.camera,
|
||||
frame,
|
||||
[cx, cy],
|
||||
[f64::from(width), f64::from(height)],
|
||||
)
|
||||
}
|
||||
|
||||
/// The curve under `(cx, cy)` and the sketch-plane point the cursor is
|
||||
/// over, or `None` if either is missing.
|
||||
///
|
||||
@@ -184,13 +265,18 @@ mod tests {
|
||||
at: Option<(DocEntityId, [f64; 2])>,
|
||||
tool: SketchTool,
|
||||
pending_jobs: usize,
|
||||
last: Option<(DocEntityId, [f64; 2], SketchTool)>,
|
||||
last: Option<CutQuestion>,
|
||||
) -> HoverStep {
|
||||
crate::preview::hover_step(
|
||||
cut_question(at, tool),
|
||||
cut_question(at, tool, Some(2.0)),
|
||||
pending_jobs,
|
||||
last,
|
||||
|(curve, near, tool)| Edit::CutHover { curve, near, tool },
|
||||
|(curve, near, tool, snap_tolerance_mm)| Edit::CutHover {
|
||||
curve,
|
||||
near,
|
||||
tool,
|
||||
snap_tolerance_mm,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -221,7 +307,7 @@ mod tests {
|
||||
cut_hover_step(None, SketchTool::Trim, 0, None),
|
||||
HoverStep::Clear
|
||||
));
|
||||
let last = Some((id(1), [1.0, 2.0], SketchTool::Trim));
|
||||
let last = Some((id(1), [1.0, 2.0], SketchTool::Trim, Some(2.0)));
|
||||
assert!(
|
||||
matches!(
|
||||
cut_hover_step(None, SketchTool::Trim, 0, last),
|
||||
@@ -258,7 +344,7 @@ mod tests {
|
||||
#[test]
|
||||
fn a_hover_that_has_not_moved_asks_nothing() {
|
||||
let at = Some((id(1), [1.0, 2.0]));
|
||||
let last = Some((id(1), [1.0, 2.0], SketchTool::Trim));
|
||||
let last = Some((id(1), [1.0, 2.0], SketchTool::Trim, Some(2.0)));
|
||||
assert!(matches!(
|
||||
cut_hover_step(at, SketchTool::Trim, 0, last),
|
||||
HoverStep::Keep
|
||||
@@ -286,7 +372,7 @@ mod tests {
|
||||
#[test]
|
||||
fn switching_tools_over_the_same_point_asks_again() {
|
||||
let at = Some((id(1), [1.0, 2.0]));
|
||||
let last = Some((id(1), [1.0, 2.0], SketchTool::Trim));
|
||||
let last = Some((id(1), [1.0, 2.0], SketchTool::Trim, Some(2.0)));
|
||||
match cut_hover_step(at, SketchTool::Split, 0, last) {
|
||||
HoverStep::Ask(Edit::CutHover { tool, .. }) => {
|
||||
assert_eq!(tool, SketchTool::Split);
|
||||
|
||||
@@ -874,6 +874,8 @@ pub(crate) enum Edit {
|
||||
/// The click, in the active sketch's own coordinates. UNSNAPPED,
|
||||
/// exactly as [`Edit::SketchDraw`]'s is and for the same reason.
|
||||
near: [f64; 2],
|
||||
/// Camera-derived capture radius; None disables optional point snapping.
|
||||
snap_tolerance_mm: Option<f64>,
|
||||
},
|
||||
/// Split `curve` into two curves at a click inside its span.
|
||||
SplitCurve {
|
||||
@@ -898,6 +900,8 @@ pub(crate) enum Edit {
|
||||
near: [f64; 2],
|
||||
/// Which of the two cut tools is armed.
|
||||
tool: SketchTool,
|
||||
/// The same capture radius the corresponding trim click will use.
|
||||
snap_tolerance_mm: Option<f64>,
|
||||
},
|
||||
/// Re-type the number of one dimension the user clicked in the
|
||||
/// viewport.
|
||||
|
||||
@@ -68,6 +68,10 @@ pub(crate) struct Scene {
|
||||
/// Failure of the final compile, after any rollback. An edit refusal whose
|
||||
/// rollback rebuilt successfully is not a broken current document.
|
||||
pub(crate) compile_error: Option<String>,
|
||||
/// Echo of a background cut-preview request. The app accepts the preview
|
||||
/// only for its still-current question and preserves earlier edit refusals
|
||||
/// when this background request succeeds.
|
||||
pub(crate) cut_hover: Option<crate::cut::CutQuestion>,
|
||||
/// `Some` only when this scene answered an
|
||||
/// [`Edit::ResolveFaceKey`](crate::edit::Edit::ResolveFaceKey).
|
||||
///
|
||||
|
||||
@@ -155,14 +155,18 @@ pub(crate) fn apply(server: &mut DocumentServer, edit: &Edit) -> Result<bool, St
|
||||
.map_err(stringy)
|
||||
}
|
||||
// ---- M3 lane B: the cut tools ------------------------------------
|
||||
Edit::TrimCurve { curve, near } => {
|
||||
Edit::TrimCurve {
|
||||
curve,
|
||||
near,
|
||||
snap_tolerance_mm,
|
||||
} => {
|
||||
let Some(sketch) = server.sketch_active else {
|
||||
return Ok(false);
|
||||
};
|
||||
let FeaturePayload::Sketch(data) = &server.document.features()[&sketch].payload else {
|
||||
return Err("the active sketch is not a sketch".to_owned());
|
||||
};
|
||||
match trim_route(data, *curve, *near) {
|
||||
match trim_route(data, *curve, *near, *snap_tolerance_mm) {
|
||||
CutRoute::Repoint { end, point } => server
|
||||
.document
|
||||
.execute(&vernier_doc::RepointCurveEndpoint {
|
||||
@@ -255,6 +259,7 @@ pub(crate) fn trim_route(
|
||||
data: &vernier_doc::SketchData,
|
||||
curve: vernier_doc::EntityId,
|
||||
near: [f64; 2],
|
||||
snap_tolerance_mm: Option<f64>,
|
||||
) -> CutRoute {
|
||||
let plan = match vernier_doc::plan_trim_curve_endpoint(data, curve, near) {
|
||||
Ok(plan) => plan,
|
||||
@@ -264,22 +269,36 @@ pub(crate) fn trim_route(
|
||||
// would.
|
||||
Err(err) => return CutRoute::Refused(err.to_string()),
|
||||
};
|
||||
let own = data
|
||||
.curves
|
||||
.get(&curve)
|
||||
.and_then(vernier_doc::SketchCurve::endpoints);
|
||||
let snap = vernier_ui::snap::snap_point(
|
||||
near,
|
||||
data,
|
||||
None,
|
||||
&vernier_ui::snap::SnapSettings {
|
||||
tolerance_mm: super::TRIM_SNAP_MM,
|
||||
tolerance_mm: snap_tolerance_mm.unwrap_or(0.0),
|
||||
..vernier_ui::snap::SnapSettings::default()
|
||||
},
|
||||
);
|
||||
let Some(point) = snap.point else {
|
||||
// This branch commits the projected carrier point. Disabling snap
|
||||
// must not duplicate an unchanged endpoint, even when an off-curve
|
||||
// cursor projects onto it. A foreign snapped point takes the other
|
||||
// branch below: its identity, not the raw projection, is the target.
|
||||
if own.is_some_and(|(start, end)| {
|
||||
[start, end].into_iter().any(|id| {
|
||||
data.points.get(&id).is_some_and(|point| {
|
||||
(point[0] - plan.new_point[0]).hypot(point[1] - plan.new_point[1])
|
||||
<= vernier_doc::POINT_COINCIDENCE_MM
|
||||
})
|
||||
})
|
||||
}) {
|
||||
return CutRoute::Refused(OWN_ENDPOINT.to_owned());
|
||||
}
|
||||
return CutRoute::Trim;
|
||||
};
|
||||
let own = data
|
||||
.curves
|
||||
.get(&curve)
|
||||
.and_then(vernier_doc::SketchCurve::endpoints);
|
||||
if own.is_some_and(|(start, end)| point == start || point == end) {
|
||||
return CutRoute::Refused(OWN_ENDPOINT.to_owned());
|
||||
}
|
||||
@@ -308,6 +327,7 @@ pub(crate) fn cut_preview(
|
||||
curve: vernier_doc::EntityId,
|
||||
near: [f64; 2],
|
||||
tool: SketchTool,
|
||||
snap_tolerance_mm: Option<f64>,
|
||||
) -> vernier_ui::hover::CutPreview {
|
||||
let refused = |why: String| vernier_ui::hover::CutPreview {
|
||||
at: near,
|
||||
@@ -342,7 +362,7 @@ pub(crate) fn cut_preview(
|
||||
};
|
||||
data.points.get(&id).copied()
|
||||
};
|
||||
match trim_route(data, curve, near) {
|
||||
match trim_route(data, curve, near, snap_tolerance_mm) {
|
||||
CutRoute::Refused(why) => refused(why),
|
||||
CutRoute::Repoint { end, point } => vernier_ui::hover::CutPreview {
|
||||
at: data.points.get(&point).copied().unwrap_or(near),
|
||||
|
||||
@@ -648,18 +648,26 @@ impl DocumentServer {
|
||||
// It is `None` for every other edit, deliberately: a stale mark
|
||||
// left standing under a moved cursor is a preview that has stopped
|
||||
// being about anything.
|
||||
let cut_preview =
|
||||
match &edit {
|
||||
Edit::CutHover { curve, near, tool } => self.sketch_active.and_then(|sketch| {
|
||||
match &self.document.features()[&sketch].payload {
|
||||
FeaturePayload::Sketch(data) => {
|
||||
Some(apply_curve_edit::cut_preview(data, *curve, *near, *tool))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
let cut_preview = match &edit {
|
||||
Edit::CutHover {
|
||||
curve,
|
||||
near,
|
||||
tool,
|
||||
snap_tolerance_mm,
|
||||
} => self.sketch_active.and_then(|sketch| {
|
||||
match &self.document.features()[&sketch].payload {
|
||||
FeaturePayload::Sketch(data) => Some(apply_curve_edit::cut_preview(
|
||||
data,
|
||||
*curve,
|
||||
*near,
|
||||
*tool,
|
||||
*snap_tolerance_mm,
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// THE TANGENT CHAIN, answered off the body this scene describes.
|
||||
// `None` rather than an empty vector when the edge is not in this
|
||||
@@ -687,6 +695,15 @@ impl DocumentServer {
|
||||
_ => None,
|
||||
};
|
||||
Scene {
|
||||
cut_hover: match &edit {
|
||||
Edit::CutHover {
|
||||
curve,
|
||||
near,
|
||||
tool,
|
||||
snap_tolerance_mm,
|
||||
} => Some((*curve, *near, *tool, *snap_tolerance_mm)),
|
||||
_ => None,
|
||||
},
|
||||
compile_error: final_compile_error,
|
||||
document_path,
|
||||
document_opened: matches!(edit, Edit::OpenDocument { .. }) && edited.is_ok(),
|
||||
@@ -947,11 +964,6 @@ impl DocumentServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy trim-endpoint capture radius. Drawing clicks now carry a
|
||||
/// screen-derived tolerance in Edit::SketchDraw; trim still uses this
|
||||
/// fixed model-space tolerance and needs the same conversion separately.
|
||||
pub(crate) const TRIM_SNAP_MM: f64 = 2.0;
|
||||
|
||||
// Keep test-only items after production: reachability scans to the first cfg(test).
|
||||
#[cfg(test)]
|
||||
use checkpoint::compile_guard::compile_document;
|
||||
|
||||
@@ -73,6 +73,7 @@ fn the_trim_tool_moves_the_nearer_end() {
|
||||
let curves_before = sketch_data(&server).curves.len();
|
||||
|
||||
let scene = server.handle(Edit::TrimCurve {
|
||||
snap_tolerance_mm: Some(2.0),
|
||||
curve: bottom,
|
||||
near: [12.0, 0.4],
|
||||
});
|
||||
@@ -122,6 +123,7 @@ fn a_trim_click_that_snaps_to_an_existing_point_repoints_instead_of_minting_one(
|
||||
let points_before = sketch_data(&server).points.len();
|
||||
|
||||
let scene = server.handle(Edit::TrimCurve {
|
||||
snap_tolerance_mm: Some(2.0),
|
||||
curve: bottom,
|
||||
near: [0.2, 14.9],
|
||||
});
|
||||
@@ -154,6 +156,7 @@ fn a_trim_click_on_the_curves_own_corner_refuses_and_leaves_the_profile_closed()
|
||||
let points_before = sketch_data(&server).points.len();
|
||||
|
||||
let scene = server.handle(Edit::TrimCurve {
|
||||
snap_tolerance_mm: Some(2.0),
|
||||
curve: bottom,
|
||||
near: [0.1, 0.1],
|
||||
});
|
||||
@@ -250,6 +253,7 @@ fn the_hover_preview_names_the_same_endpoint_the_commit_moves() {
|
||||
let bottom = curve_between(&server, [0.0, 0.0], [20.0, 0.0]);
|
||||
|
||||
let scene = server.handle(Edit::CutHover {
|
||||
snap_tolerance_mm: Some(2.0),
|
||||
curve: bottom,
|
||||
near: [12.0, 0.4],
|
||||
tool: SketchTool::Trim,
|
||||
@@ -267,6 +271,7 @@ fn the_hover_preview_names_the_same_endpoint_the_commit_moves() {
|
||||
);
|
||||
|
||||
let scene = server.handle(Edit::TrimCurve {
|
||||
snap_tolerance_mm: Some(2.0),
|
||||
curve: bottom,
|
||||
near: [12.0, 0.4],
|
||||
});
|
||||
@@ -291,6 +296,7 @@ fn the_hover_preview_carries_the_same_refusal_the_click_would_raise() {
|
||||
let bottom = curve_between(&server, [0.0, 0.0], [20.0, 0.0]);
|
||||
|
||||
let scene = server.handle(Edit::CutHover {
|
||||
snap_tolerance_mm: Some(2.0),
|
||||
curve: bottom,
|
||||
near: [0.1, 0.1],
|
||||
tool: SketchTool::Trim,
|
||||
@@ -303,6 +309,7 @@ fn the_hover_preview_carries_the_same_refusal_the_click_would_raise() {
|
||||
);
|
||||
|
||||
let scene = server.handle(Edit::TrimCurve {
|
||||
snap_tolerance_mm: Some(2.0),
|
||||
curve: bottom,
|
||||
near: [0.1, 0.1],
|
||||
});
|
||||
@@ -320,6 +327,7 @@ fn a_split_preview_marks_the_point_and_draws_no_ghost() {
|
||||
let (mut server, _scene) = rectangle_sketch();
|
||||
let bottom = curve_between(&server, [0.0, 0.0], [20.0, 0.0]);
|
||||
let scene = server.handle(Edit::CutHover {
|
||||
snap_tolerance_mm: Some(2.0),
|
||||
curve: bottom,
|
||||
near: [10.0, 0.3],
|
||||
tool: SketchTool::Split,
|
||||
@@ -338,16 +346,19 @@ fn the_trim_branch_table_answers_all_three_rows() {
|
||||
let bottom = curve_between(&server, [0.0, 0.0], [20.0, 0.0]);
|
||||
let data = sketch_data(&server);
|
||||
|
||||
assert_eq!(trim_route(data, bottom, [12.0, 0.4]), CutRoute::Trim);
|
||||
assert_eq!(
|
||||
trim_route(data, bottom, [12.0, 0.4], Some(2.0)),
|
||||
CutRoute::Trim
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
trim_route(data, bottom, [0.2, 14.9]),
|
||||
trim_route(data, bottom, [0.2, 14.9], Some(2.0)),
|
||||
CutRoute::Repoint { .. }
|
||||
),
|
||||
"a snap onto a foreign point must repoint"
|
||||
);
|
||||
assert_eq!(
|
||||
trim_route(data, bottom, [0.1, 0.1]),
|
||||
trim_route(data, bottom, [0.1, 0.1], Some(2.0)),
|
||||
CutRoute::Refused(OWN_ENDPOINT.to_owned())
|
||||
);
|
||||
}
|
||||
@@ -395,7 +406,7 @@ fn app_with_a_standing_cut_preview() -> crate::VernierApp {
|
||||
.created
|
||||
.first()
|
||||
.expect("one id");
|
||||
app.cut_hover_at = Some((curve, [12.0, 0.0], SketchTool::Trim));
|
||||
app.cut_hover_at = Some((curve, [12.0, 0.0], SketchTool::Trim, Some(2.0)));
|
||||
app.scene.cut_preview = Some(vernier_ui::hover::CutPreview {
|
||||
at: [12.0, 0.0],
|
||||
from: Some([20.0, 0.0]),
|
||||
@@ -468,6 +479,7 @@ fn a_hover_answer_that_arrives_after_the_cursor_left_is_dropped() {
|
||||
};
|
||||
app.apply_scene(Scene {
|
||||
compile_error: None,
|
||||
cut_hover: None,
|
||||
document_path: None,
|
||||
document_opened: false,
|
||||
extrusion_applied: false,
|
||||
@@ -489,3 +501,401 @@ fn a_hover_answer_that_arrives_after_the_cursor_left_is_dropped() {
|
||||
"a preview no standing hover asked for must not be painted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_real_cut_preview_survives_without_a_drawing_hover() {
|
||||
let (mut server, _) = rectangle_sketch();
|
||||
let curve = curve_between(&server, [0.0, 0.0], [20.0, 0.0]);
|
||||
let scene = server.handle(Edit::CutHover {
|
||||
curve,
|
||||
near: [12.0, 0.4],
|
||||
tool: SketchTool::Trim,
|
||||
snap_tolerance_mm: Some(2.0),
|
||||
});
|
||||
assert!(scene.view.cut_preview.is_some(), "server positive control");
|
||||
let mut app = app_with_a_standing_cut_preview();
|
||||
app.cut_hover_at = Some((curve, [12.0, 0.4], SketchTool::Trim, Some(2.0)));
|
||||
app.hover_at = None;
|
||||
app.apply_scene(scene);
|
||||
assert!(
|
||||
app.scene.cut_preview.is_some(),
|
||||
"a cut answer belongs to its cut question, not drawing hover"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_trim_snap_preserves_the_projected_own_end_invariant_and_all_history() {
|
||||
let (mut server, _) = rectangle_sketch();
|
||||
let curve = curve_between(&server, [0.0, 0.0], [20.0, 0.0]);
|
||||
let before = server.document.to_session_json().unwrap();
|
||||
for tolerance in [None, Some(0.0), Some(0.2)] {
|
||||
for near in [
|
||||
[0.0, 0.0],
|
||||
[20.0, 0.0],
|
||||
[0.0, 0.1],
|
||||
[20.0, -0.1],
|
||||
[0.5e-7, 0.0],
|
||||
] {
|
||||
let preview = server.handle(Edit::CutHover {
|
||||
curve,
|
||||
near,
|
||||
tool: SketchTool::Trim,
|
||||
snap_tolerance_mm: tolerance,
|
||||
});
|
||||
assert_eq!(
|
||||
preview.view.cut_preview.unwrap().refusal.as_deref(),
|
||||
Some(OWN_ENDPOINT)
|
||||
);
|
||||
let dirty = preview.view.unsaved;
|
||||
let result = server.handle(Edit::TrimCurve {
|
||||
curve,
|
||||
near,
|
||||
snap_tolerance_mm: tolerance,
|
||||
});
|
||||
assert_eq!(result.error.as_deref(), Some(OWN_ENDPOINT));
|
||||
assert_eq!(result.view.unsaved, dirty);
|
||||
assert_eq!(
|
||||
server.document.to_session_json().unwrap(),
|
||||
before,
|
||||
"refusal must not allocate or alter either history stack: {near:?}, {tolerance:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_trim_snap_uses_the_preview_plan_and_retains_curve_identity() {
|
||||
for (near, tolerance) in [([0.1, 0.02], None), ([0.5, 0.0], Some(0.2))] {
|
||||
// Fixture construction uses real server drawing commands, with the
|
||||
// optional draw snap disabled so this is exactly a 1 mm rectangle.
|
||||
let mut server = DocumentServer::empty();
|
||||
server.handle(Edit::NewSketch {
|
||||
plane: crate::edit::NewSketchPlane::World(vernier_doc::PrincipalPlane::Xy),
|
||||
});
|
||||
for at in [[0.0, 0.0], [1.0, 1.0]] {
|
||||
let scene = server.handle(Edit::SketchDraw {
|
||||
at,
|
||||
tool: SketchTool::Rectangle,
|
||||
snap_tolerance_mm: None,
|
||||
infer: false,
|
||||
sides: 6,
|
||||
});
|
||||
assert!(scene.error.is_none(), "{:?}", scene.error);
|
||||
}
|
||||
let curve = curve_between(&server, [0.0, 0.0], [1.0, 0.0]);
|
||||
let before = sketch_data(&server).clone();
|
||||
if tolerance.is_none() {
|
||||
assert!(
|
||||
matches!(
|
||||
trim_route(&before, curve, near, Some(0.2)),
|
||||
CutRoute::Refused(_)
|
||||
),
|
||||
"positive control: optional capture would refuse this click"
|
||||
);
|
||||
}
|
||||
let preview = server
|
||||
.handle(Edit::CutHover {
|
||||
curve,
|
||||
near,
|
||||
tool: SketchTool::Trim,
|
||||
snap_tolerance_mm: tolerance,
|
||||
})
|
||||
.view
|
||||
.cut_preview
|
||||
.unwrap();
|
||||
assert!(preview.refusal.is_none());
|
||||
let result = server.handle(Edit::TrimCurve {
|
||||
curve,
|
||||
near,
|
||||
snap_tolerance_mm: tolerance,
|
||||
});
|
||||
assert!(result.error.is_none(), "{:?}", result.error);
|
||||
let after = sketch_data(&server);
|
||||
assert_eq!(
|
||||
after.curves.keys().collect::<Vec<_>>(),
|
||||
before.curves.keys().collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(after.points.len(), before.points.len() + 1);
|
||||
let added: Vec<_> = after
|
||||
.points
|
||||
.iter()
|
||||
.filter(|(id, _)| !before.points.contains_key(*id))
|
||||
.collect();
|
||||
assert_eq!(added.len(), 1);
|
||||
assert_eq!(
|
||||
*added[0].1, preview.at,
|
||||
"preview and commit share the exact carrier point"
|
||||
);
|
||||
assert_eq!(preview.at, [near[0], 0.0]);
|
||||
for (id, at) in &before.points {
|
||||
assert_eq!(after.points[id], *at);
|
||||
}
|
||||
let sketch = server.sketch_active.unwrap();
|
||||
server.handle(Edit::UndoRedo { back: true });
|
||||
assert_eq!(
|
||||
server.document.features()[&sketch].payload,
|
||||
FeaturePayload::Sketch(before),
|
||||
"undo restores the original endpoints and topology"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stale_tolerance_answer_is_dropped_and_a_successful_hover_retains_an_edit_refusal() {
|
||||
let (mut server, _) = rectangle_sketch();
|
||||
let curve = curve_between(&server, [0.0, 0.0], [20.0, 0.0]);
|
||||
let mut app = app_with_a_standing_cut_preview();
|
||||
app.last_error = Some("previous edit refused".to_owned());
|
||||
app.shell.readout = "previous refusal details".to_owned();
|
||||
app.cut_hover_at = Some((curve, [12.0, 0.4], SketchTool::Trim, None));
|
||||
app.apply_scene(server.handle(Edit::CutHover {
|
||||
curve,
|
||||
near: [12.0, 0.4],
|
||||
tool: SketchTool::Trim,
|
||||
snap_tolerance_mm: Some(2.0),
|
||||
}));
|
||||
assert!(
|
||||
app.scene.cut_preview.is_none(),
|
||||
"a previous snap radius must not paint under Alt"
|
||||
);
|
||||
assert_eq!(app.last_error.as_deref(), Some("previous edit refused"));
|
||||
assert_eq!(app.shell.readout, "previous refusal details");
|
||||
let mut failure = server.handle(Edit::CutHover {
|
||||
curve,
|
||||
near: [12.0, 0.4],
|
||||
tool: SketchTool::Trim,
|
||||
snap_tolerance_mm: None,
|
||||
});
|
||||
failure.error = Some("current compile failed".to_owned());
|
||||
failure.compile_error = failure.error.clone();
|
||||
app.apply_scene(failure);
|
||||
assert_eq!(
|
||||
app.last_error.as_deref(),
|
||||
Some("current compile failed"),
|
||||
"background compile failures must still surface"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn held_hover_retries_once_with_current_modifiers_and_respects_frame_barriers() {
|
||||
super::frame::bounded_drive("held cut preview refresh", || {
|
||||
let mut server = DocumentServer::empty();
|
||||
server.handle(Edit::NewSketch {
|
||||
plane: crate::edit::NewSketchPlane::World(vernier_doc::PrincipalPlane::Xy),
|
||||
});
|
||||
for at in [[0.0, 0.0], [20.0, 15.0]] {
|
||||
server.handle(Edit::SketchDraw {
|
||||
at,
|
||||
snap_tolerance_mm: None,
|
||||
tool: SketchTool::Rectangle,
|
||||
infer: false,
|
||||
sides: 6,
|
||||
});
|
||||
}
|
||||
let initial = server.handle(Edit::Recompute);
|
||||
let (release, gate) = std::sync::mpsc::channel::<()>();
|
||||
let (requests, received) = std::sync::mpsc::channel();
|
||||
let worker = Worker::spawn(
|
||||
"held-cut-preview",
|
||||
move |edit| {
|
||||
requests
|
||||
.send(matches!(edit, Edit::CutHover { .. }))
|
||||
.unwrap();
|
||||
gate.recv_timeout(std::time::Duration::from_secs(5))
|
||||
.unwrap();
|
||||
server.reply(edit)
|
||||
},
|
||||
|| {},
|
||||
)
|
||||
.unwrap();
|
||||
let mut app = VernierApp::new(worker);
|
||||
app.offscreen = Some(vernier_render::Offscreen::new(1600, 1000).unwrap());
|
||||
app.apply_scene(initial);
|
||||
app.camera = OrbitCamera::framing([10.0, 7.5, 0.0], 120.0);
|
||||
app.shell.tool = SketchTool::Trim;
|
||||
let at = vernier_ui::gizmo::project(
|
||||
&app.camera.view_proj(1.6),
|
||||
[12.0, 0.0, 0.0],
|
||||
[1600.0, 1000.0],
|
||||
)
|
||||
.unwrap();
|
||||
app.input.cursor = Some((f64::from(at[0]), f64::from(at[1])));
|
||||
let mut index = 0_u32;
|
||||
let mut frame = |app: &mut VernierApp, pointer: Option<[f32; 2]>| {
|
||||
let mut input = egui::RawInput {
|
||||
screen_rect: Some(egui::Rect::from_min_size(
|
||||
egui::Pos2::ZERO,
|
||||
egui::vec2(1600.0, 1000.0),
|
||||
)),
|
||||
time: Some(f64::from(index) / 60.0),
|
||||
focused: true,
|
||||
..Default::default()
|
||||
};
|
||||
index += 1;
|
||||
if let Some([x, y]) = pointer {
|
||||
input
|
||||
.events
|
||||
.push(egui::Event::PointerMoved(egui::pos2(x, y)));
|
||||
}
|
||||
let mut output = app.run_shell_frame(input, [1600.0, 1000.0]);
|
||||
output.textures_delta.clear();
|
||||
};
|
||||
let finish = |app: &mut VernierApp| {
|
||||
release.send(()).unwrap();
|
||||
let reply = app
|
||||
.worker
|
||||
.bounded()
|
||||
.recv_timeout(std::time::Duration::from_secs(5))
|
||||
.unwrap();
|
||||
app.apply_reply(reply);
|
||||
};
|
||||
assert!(
|
||||
app.cut_target(f64::from(at[0]), f64::from(at[1]), app.sketch_mode.unwrap())
|
||||
.is_some(),
|
||||
"fixture cursor must hit a curve"
|
||||
);
|
||||
frame(&mut app, Some(at));
|
||||
frame(&mut app, None);
|
||||
assert!(
|
||||
received
|
||||
.recv_timeout(std::time::Duration::from_secs(2))
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(app.pending_jobs, 1);
|
||||
assert!(app.cut_hover_at.unwrap().3.is_some());
|
||||
app.input.modifiers = winit::keyboard::ModifiersState::ALT;
|
||||
frame(&mut app, None);
|
||||
assert_eq!(
|
||||
app.pending_jobs, 1,
|
||||
"a held worker must not queue another hover"
|
||||
);
|
||||
assert!(app.cut_hover_at.is_none() && app.scene.cut_preview.is_none());
|
||||
finish(&mut app);
|
||||
assert!(
|
||||
app.scene.cut_preview.is_none(),
|
||||
"old-radius response must be rejected"
|
||||
);
|
||||
frame(&mut app, None);
|
||||
assert!(
|
||||
received
|
||||
.recv_timeout(std::time::Duration::from_secs(2))
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(app.cut_hover_at.unwrap().3, None);
|
||||
finish(&mut app);
|
||||
let preview = app
|
||||
.scene
|
||||
.cut_preview
|
||||
.clone()
|
||||
.expect("the idle frame retries the invalidated request");
|
||||
assert!(preview.refusal.is_none());
|
||||
for _ in 0..5 {
|
||||
frame(&mut app, None);
|
||||
}
|
||||
assert_eq!(
|
||||
app.pending_jobs, 0,
|
||||
"a stable answer must not trigger an unbounded recompute loop"
|
||||
);
|
||||
assert!(matches!(
|
||||
received.try_recv(),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty)
|
||||
));
|
||||
assert_eq!(app.scene.cut_preview, Some(preview));
|
||||
|
||||
// egui owns the pointer over the actual timeline panel. The prior
|
||||
// nonempty preview is a positive control for this invalidation.
|
||||
app.input.cursor = Some((100.0, 300.0));
|
||||
frame(&mut app, Some([100.0, 300.0]));
|
||||
assert!(app.scene.cut_preview.is_none() && app.cut_hover_at.is_none());
|
||||
assert_eq!(app.pending_jobs, 0);
|
||||
app.input.cursor = Some((f64::from(at[0]), f64::from(at[1])));
|
||||
frame(&mut app, Some(at));
|
||||
assert!(
|
||||
received
|
||||
.recv_timeout(std::time::Duration::from_secs(2))
|
||||
.unwrap()
|
||||
);
|
||||
finish(&mut app);
|
||||
assert!(app.scene.cut_preview.is_some());
|
||||
|
||||
// A real submitted Save is held across a stationary frame: no
|
||||
// hover may cross its document barrier, even after invalidation.
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"vernier-cut-barrier-{}.vernier",
|
||||
std::process::id()
|
||||
));
|
||||
app.submit(Edit::SaveDocument {
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
});
|
||||
assert!(
|
||||
!received
|
||||
.recv_timeout(std::time::Duration::from_secs(2))
|
||||
.unwrap()
|
||||
);
|
||||
app.input.modifiers = winit::keyboard::ModifiersState::empty();
|
||||
frame(&mut app, None);
|
||||
assert_eq!(app.pending_jobs, 1);
|
||||
assert!(matches!(
|
||||
received.try_recv(),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty)
|
||||
));
|
||||
finish(&mut app);
|
||||
app.request_close();
|
||||
frame(&mut app, None);
|
||||
assert_eq!(
|
||||
app.pending_jobs, 0,
|
||||
"closing must not start background work"
|
||||
);
|
||||
assert!(matches!(
|
||||
received.try_recv(),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty)
|
||||
));
|
||||
std::fs::remove_file(&path).unwrap();
|
||||
std::fs::remove_file(vernier_ui::store_path(&path.to_string_lossy())).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_foreign_snap_target_takes_precedence_over_the_raw_cursor_projection() {
|
||||
let (mut server, _) = rectangle_sketch();
|
||||
let bottom = curve_between(&server, [0.0, 0.0], [20.0, 0.0]);
|
||||
let before = sketch_data(&server).clone();
|
||||
let near = [0.0, 15.0];
|
||||
let foreign = *before.points.iter().find(|(_, at)| **at == near).unwrap().0;
|
||||
assert!(
|
||||
matches!(trim_route(&before, bottom, near, Some(2.0)), CutRoute::Repoint { point, .. } if point == foreign),
|
||||
"the actual target is the foreign snapped point, even when the raw cursor projects onto an own endpoint"
|
||||
);
|
||||
assert_eq!(
|
||||
trim_route(&before, bottom, near, None),
|
||||
CutRoute::Refused(OWN_ENDPOINT.to_owned()),
|
||||
"without snap the actual target is the unchanged carrier endpoint"
|
||||
);
|
||||
let preview = server
|
||||
.handle(Edit::CutHover {
|
||||
curve: bottom,
|
||||
near,
|
||||
tool: SketchTool::Trim,
|
||||
snap_tolerance_mm: Some(2.0),
|
||||
})
|
||||
.view
|
||||
.cut_preview
|
||||
.unwrap();
|
||||
assert_eq!(preview.at, near);
|
||||
assert!(preview.refusal.is_none());
|
||||
let result = server.handle(Edit::TrimCurve {
|
||||
curve: bottom,
|
||||
near,
|
||||
snap_tolerance_mm: Some(2.0),
|
||||
});
|
||||
assert!(result.error.is_none(), "{:?}", result.error);
|
||||
assert_eq!(
|
||||
sketch_data(&server).points,
|
||||
before.points,
|
||||
"reuse the foreign point identity; mint nothing"
|
||||
);
|
||||
assert_eq!(
|
||||
sketch_data(&server).curves[&bottom].endpoints().unwrap().0,
|
||||
foreign
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1164,6 +1164,7 @@ fn headless_app() -> VernierApp {
|
||||
fn scene_of(view: SceneView) -> Scene {
|
||||
Scene {
|
||||
compile_error: None,
|
||||
cut_hover: None,
|
||||
document_path: None,
|
||||
document_opened: false,
|
||||
extrusion_applied: false,
|
||||
|
||||
@@ -381,6 +381,7 @@ fn an_errored_scene_keeps_the_session_and_clears_only_the_applied_token() {
|
||||
});
|
||||
app.apply_scene(Scene {
|
||||
compile_error: None,
|
||||
cut_hover: None,
|
||||
document_path: None,
|
||||
document_opened: false,
|
||||
extrusion_applied: false,
|
||||
@@ -431,6 +432,7 @@ fn an_echo_for_another_point_never_becomes_this_sessions_token() {
|
||||
});
|
||||
app.apply_scene(Scene {
|
||||
compile_error: None,
|
||||
cut_hover: None,
|
||||
document_path: None,
|
||||
document_opened: false,
|
||||
extrusion_applied: false,
|
||||
|
||||
@@ -459,6 +459,7 @@ fn an_errored_scene_retires_an_outstanding_preview_even_without_a_gizmo_in_the_f
|
||||
|
||||
app.apply_scene(Scene {
|
||||
compile_error: None,
|
||||
cut_hover: None,
|
||||
document_path: None,
|
||||
document_opened: false,
|
||||
extrusion_applied: false,
|
||||
@@ -503,6 +504,7 @@ fn apply_gizmo_frame_retire_submits_the_undo_and_clears_the_token() {
|
||||
|_: Edit| {
|
||||
crate::scene::ServerReply::Scene(Box::new(Scene {
|
||||
compile_error: None,
|
||||
cut_hover: None,
|
||||
document_path: None,
|
||||
document_opened: false,
|
||||
extrusion_applied: false,
|
||||
|
||||
@@ -1534,3 +1534,251 @@ fn save_and_close_can_preserve_a_document_whose_redone_fillet_fails_compile() {
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_trim_hover_tracks_stationary_alt_and_camera_and_matches_the_click() {
|
||||
bounded("trim-stationary-hover", || {
|
||||
for distance in [40.0, 80.0] {
|
||||
let mut headless = Headless::empty(1600, 1000).unwrap();
|
||||
*headless.camera_mut() =
|
||||
vernier_render::OrbitCamera::framing([0.0, 0.0, 0.0], distance);
|
||||
palette(&mut headless, "sketch", None);
|
||||
label(&mut headless, "more");
|
||||
label(&mut headless, "Rectangle");
|
||||
for world in [[0.0, 0.0, 0.0], [1.0, 1.0, 0.0]] {
|
||||
let [x, y] = headless.world_pixel(world).unwrap();
|
||||
click(&mut headless, egui::pos2(x, y));
|
||||
headless.wait_idle(Headless::DEFAULT_BUDGET).unwrap();
|
||||
}
|
||||
assert!(headless.view().closed_profile.is_some());
|
||||
label(&mut headless, "more");
|
||||
label(&mut headless, "Trim");
|
||||
let [x, y] = headless.world_pixel([0.1, 0.0, 0.0]).unwrap();
|
||||
headless.set_cursor(x, y);
|
||||
headless
|
||||
.step_events(vec![egui::Event::PointerMoved(egui::pos2(x, y))], false)
|
||||
.unwrap();
|
||||
headless.wait_idle(Headless::DEFAULT_BUDGET).unwrap();
|
||||
let preview = headless
|
||||
.view()
|
||||
.cut_preview
|
||||
.as_ref()
|
||||
.expect("real shared frame must request and retain a positive cut preview");
|
||||
assert!(
|
||||
preview
|
||||
.refusal
|
||||
.as_ref()
|
||||
.is_some_and(|error| error.contains("own end")),
|
||||
"six-pixel capture applies before Alt: {preview:?}"
|
||||
);
|
||||
|
||||
let painted = headless.step_events(Vec::new(), false).unwrap();
|
||||
assert!(
|
||||
painted
|
||||
.texts
|
||||
.iter()
|
||||
.any(|text| text.text.contains("own end")),
|
||||
"positive control: refusal is actually painted"
|
||||
);
|
||||
// Open the real command palette over the stationary curve cursor.
|
||||
// Inspect every returned frame: clearing state after drawing must
|
||||
// not leave a refusal glyph stuck on the rendered surface.
|
||||
headless.set_modifiers(true, false);
|
||||
let mut checked_consumed_frame = false;
|
||||
for events in [
|
||||
vec![
|
||||
egui::Event::ModifiersChanged(headless.modifiers()),
|
||||
egui::Event::Key {
|
||||
key: egui::Key::K,
|
||||
physical_key: None,
|
||||
pressed: true,
|
||||
repeat: false,
|
||||
modifiers: egui::Modifiers {
|
||||
ctrl: true,
|
||||
command: true,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
],
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
] {
|
||||
let painted = headless.step_events(events, false).unwrap();
|
||||
if headless.view().cut_preview.is_none() {
|
||||
checked_consumed_frame = true;
|
||||
assert!(
|
||||
!painted
|
||||
.texts
|
||||
.iter()
|
||||
.any(|text| text.text.contains("own end")),
|
||||
"the same consumed-pointer frame must omit the cut glyph"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(headless.shell().command_bar.open);
|
||||
assert!(
|
||||
checked_consumed_frame,
|
||||
"palette must cover the held curve cursor and clear a previously painted preview"
|
||||
);
|
||||
headless.set_modifiers(false, false);
|
||||
headless
|
||||
.step_events(
|
||||
vec![egui::Event::ModifiersChanged(headless.modifiers())],
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
key(&mut headless, egui::Key::Escape, false);
|
||||
headless.step_events(Vec::new(), false).unwrap();
|
||||
headless.wait_idle(Headless::DEFAULT_BUDGET).unwrap();
|
||||
assert!(
|
||||
headless.view().cut_preview.is_some(),
|
||||
"stationary cursor re-asks when the palette closes"
|
||||
);
|
||||
|
||||
// No PointerMoved event: a held cursor must ask again under Alt.
|
||||
headless.set_modifiers_with_alt(false, false, true);
|
||||
headless.step_events(Vec::new(), false).unwrap();
|
||||
assert!(
|
||||
headless.view().cut_preview.is_none(),
|
||||
"clear old answer before replacement arrives"
|
||||
);
|
||||
headless.wait_idle(Headless::DEFAULT_BUDGET).unwrap();
|
||||
let first = headless
|
||||
.view()
|
||||
.cut_preview
|
||||
.clone()
|
||||
.expect("stationary Alt must request a fresh preview");
|
||||
assert!(first.refusal.is_none(), "{first:?}");
|
||||
|
||||
let camera = headless.camera_mut().clone();
|
||||
headless.camera_mut().zoom(2.0);
|
||||
headless.step_events(Vec::new(), false).unwrap();
|
||||
assert!(headless.view().cut_preview.is_none());
|
||||
headless.wait_idle(Headless::DEFAULT_BUDGET).unwrap();
|
||||
let zoomed = headless
|
||||
.view()
|
||||
.cut_preview
|
||||
.clone()
|
||||
.expect("stationary camera change must request a fresh preview");
|
||||
assert!(zoomed.refusal.is_none());
|
||||
assert!(
|
||||
(zoomed.at[0] - first.at[0]).abs() > 0.01,
|
||||
"camera must change the projected point: {first:?} / {zoomed:?}"
|
||||
);
|
||||
*headless.camera_mut() = camera;
|
||||
headless.step_events(Vec::new(), false).unwrap();
|
||||
headless.wait_idle(Headless::DEFAULT_BUDGET).unwrap();
|
||||
let expected = headless.view().cut_preview.clone().unwrap().at;
|
||||
assert_eq!(expected, first.at);
|
||||
|
||||
// Explicit save fixtures expose authored document state as an oracle;
|
||||
// the modeled operation itself is a real viewport click.
|
||||
let path = std::env::temp_dir()
|
||||
.join(format!("vernier-trim-hover-{}.vernier", std::process::id()));
|
||||
let read_sketch = |path: &std::path::Path| {
|
||||
let document =
|
||||
vernier_doc::Document::from_json(&std::fs::read_to_string(path).unwrap())
|
||||
.unwrap();
|
||||
document
|
||||
.features()
|
||||
.values()
|
||||
.find_map(|feature| match &feature.payload {
|
||||
vernier_doc::FeaturePayload::Sketch(data) => Some(data.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap()
|
||||
};
|
||||
headless.save_document(path.to_string_lossy().into_owned());
|
||||
headless.wait_idle(Headless::DEFAULT_BUDGET).unwrap();
|
||||
let before = read_sketch(&path);
|
||||
click(&mut headless, egui::pos2(x, y));
|
||||
headless.wait_idle(Headless::DEFAULT_BUDGET).unwrap();
|
||||
assert!(
|
||||
headless.last_error().is_none(),
|
||||
"{:?}",
|
||||
headless.last_error()
|
||||
);
|
||||
assert!(headless.view().closed_profile.is_none());
|
||||
headless.save_document(path.to_string_lossy().into_owned());
|
||||
headless.wait_idle(Headless::DEFAULT_BUDGET).unwrap();
|
||||
let after = read_sketch(&path);
|
||||
let trimmed_json = std::fs::read_to_string(&path).unwrap();
|
||||
assert_eq!(after.points.len(), before.points.len() + 1);
|
||||
assert_eq!(
|
||||
after.curves.keys().collect::<Vec<_>>(),
|
||||
before.curves.keys().collect::<Vec<_>>()
|
||||
);
|
||||
let added: Vec<_> = after
|
||||
.points
|
||||
.iter()
|
||||
.filter(|(id, _)| !before.points.contains_key(*id))
|
||||
.collect();
|
||||
assert_eq!(added.len(), 1);
|
||||
assert_eq!(
|
||||
*added[0].1, expected,
|
||||
"visible preview exactly matches committed point"
|
||||
);
|
||||
headless.set_modifiers_with_alt(false, false, false);
|
||||
headless.key(Taste::Zeichen('z'), true, false);
|
||||
headless.wait_idle(Headless::DEFAULT_BUDGET).unwrap();
|
||||
headless.save_document(path.to_string_lossy().into_owned());
|
||||
headless.wait_idle(Headless::DEFAULT_BUDGET).unwrap();
|
||||
assert_eq!(
|
||||
read_sketch(&path),
|
||||
before,
|
||||
"actual Undo restores every original curve and endpoint identity"
|
||||
);
|
||||
// Undo deliberately leaves sketch drawing mode. Re-selecting a
|
||||
// line profile after Undo/Open is an existing missing workflow;
|
||||
// this isolated extrusion is a geometry oracle, not a GUI claim.
|
||||
let mut restored =
|
||||
vernier_doc::Document::from_json(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
let sketch = *restored.features().keys().next().unwrap();
|
||||
let extrusion = vernier_doc::AddExtrude {
|
||||
name: "undo geometry oracle".to_owned(),
|
||||
sketch,
|
||||
profile: before.curves.keys().copied().collect(),
|
||||
height: 2.0,
|
||||
target: restored.sole_body_target(false).unwrap(),
|
||||
};
|
||||
// The oracle must reject the cut-open topology, not merely accept
|
||||
// any four curves of this shape. Use the captured committed sketch
|
||||
// in an isolated document to exercise that negative control.
|
||||
let mut open = vernier_doc::Document::from_json(&trimmed_json).unwrap();
|
||||
open.execute(&extrusion).unwrap();
|
||||
let names_json =
|
||||
std::fs::read_to_string(vernier_ui::store_path(&path.to_string_lossy())).unwrap();
|
||||
let mut open_names = vernier_ui::NamingStore::from_json(&names_json).unwrap();
|
||||
let mut open_kernel = vernier_kernel::occt::OcctKernel::new();
|
||||
let error = vernier_ui::compile_document(&mut open, &mut open_kernel, &mut open_names)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
error.to_string().contains("profile chain does not close"),
|
||||
"open profile must fail for its topology: {error}"
|
||||
);
|
||||
restored.execute(&extrusion).unwrap();
|
||||
let mut names = vernier_ui::NamingStore::from_json(
|
||||
&std::fs::read_to_string(vernier_ui::store_path(&path.to_string_lossy())).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut kernel = vernier_kernel::occt::OcctKernel::new();
|
||||
let compiled =
|
||||
vernier_ui::compile_document(&mut restored, &mut kernel, &mut names).unwrap();
|
||||
let step = path.with_extension("step");
|
||||
kernel
|
||||
.export_step(compiled.sole_body().unwrap().shape, &step.to_string_lossy())
|
||||
.unwrap();
|
||||
let imported = kernel.import_step(&step.to_string_lossy()).unwrap();
|
||||
assert_eq!(imported.summary.topology.solids, 1);
|
||||
assert_eq!(imported.summary.topology.faces, 6);
|
||||
assert!(
|
||||
(imported.summary.geometry.volume - 2.0).abs() < 1e-9,
|
||||
"1 x 1 x 2 mm restored geometry: {:?}",
|
||||
imported.summary
|
||||
);
|
||||
std::fs::remove_file(step).unwrap();
|
||||
std::fs::remove_file(&path).unwrap();
|
||||
std::fs::remove_file(vernier_ui::store_path(&path.to_string_lossy())).unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,6 +62,9 @@ fn modeling_workflows_produce_their_exported_geometry() {
|
||||
"functions-direct-edge-fillet",
|
||||
"functions-direct-edge-chamfer",
|
||||
"functions-direct-edge-chain",
|
||||
"functions-hole",
|
||||
"functions-push-pull-edit",
|
||||
"functions-pattern-count",
|
||||
"save-preserves-selection",
|
||||
"save-preserves-sketch",
|
||||
] {
|
||||
@@ -78,6 +81,27 @@ fn modeling_workflows_produce_their_exported_geometry() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Trim changes an open sketch; these scripts assert its native topology and
|
||||
/// real Undo. The headless companion supplies the restored-profile STEP oracle.
|
||||
#[test]
|
||||
fn small_trim_and_undo_preserve_native_topology_at_both_zoom_levels() {
|
||||
for name in ["small-trim-40", "small-trim-80"] {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../scripts/drive")
|
||||
.join(format!("{name}.json"));
|
||||
let report = drive(&path, &out_dir(name), Determinism::Once, options())
|
||||
.unwrap_or_else(|error| panic!("{name}: {error}"));
|
||||
assert_eq!(report.result, "pass", "{name}");
|
||||
assert!(
|
||||
report
|
||||
.trace
|
||||
.iter()
|
||||
.any(|step| step.step == "expect_document"),
|
||||
"{name} must assert the saved sketch topology"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Empty inspection still exercises the actual controls; it has no solid to export.
|
||||
#[test]
|
||||
fn inspection_controls_refuse_an_empty_document() {
|
||||
|
||||
@@ -1290,15 +1290,15 @@ pub static COMMANDS: &[CommandDescriptor] = &[
|
||||
group: "parameter",
|
||||
order: 70,
|
||||
needs: "select a pattern",
|
||||
params: &[
|
||||
ParamSpec {
|
||||
key: "count",
|
||||
unit: Unit::Count,
|
||||
range: 1.0..=64.0,
|
||||
seed: Seed::FromFeature,
|
||||
},
|
||||
mm("step", Seed::FromFeature),
|
||||
],
|
||||
// SetPatternCount edits only the instance count. A second "step"
|
||||
// field also wrote state.count and overwrote the typed count when
|
||||
// the card committed all buffers. Spacing needs its own command.
|
||||
params: &[ParamSpec {
|
||||
key: "count",
|
||||
unit: Unit::Count,
|
||||
range: 1.0..=64.0,
|
||||
seed: Seed::FromFeature,
|
||||
}],
|
||||
..BLANK
|
||||
},
|
||||
CommandDescriptor {
|
||||
|
||||
@@ -231,3 +231,21 @@ Release `save-preserves-selection.json` and `save-preserves-sketch.json` pass cr
|
||||
File status, dirty acknowledgements, close outcome and compile-diagnostic distinction are covered in the implementation report. This improves the Save row only; it does not upgrade other registry rows or eliminate the native worker's unbounded in-flight-call shutdown limitation. Independent final Save review and combined gate are recorded in subsequent evidence.
|
||||
|
||||
Final combined evidence: workspace 1,403 tests, release Headless 23 tests, release CLI 44 checks and workspace Clippy all pass; formatting passes. Independent `FILE_ONLY_SAVE_REVIEW_2026-09-08.md` closes this bounded Save change with no blocker. These totals include existing tests and are not a claim that all 73 runnable actions are now individually verified.
|
||||
|
||||
## Hole and existing push/pull parameter controls — after checkpoint 9c297b9
|
||||
|
||||
`functions-hole.json` now exercises actual Point placement/locking, two-point selection, MakeHole, EditHole diameter/depth and Through, with creation/edit undo-redo. Five STEP checks assert the analytic two-cylinder remainder; release cross-process execution passes. Counterbore still has no enable/head-diameter/head-depth controls and is explicitly unverified. See `HOLE_CONTROL_VERIFICATION_2026-09-08.md`.
|
||||
|
||||
`functions-push-pull-edit.json` extends creation coverage to selected-feature distance editing: +5 creation, +7.5 edit, undo/redo, zero-distance refusal with unchanged geometry, and -2 inward edit. All six STEP oracles match `40*30*(15+d)` with one six-face solid; the release double run passes and a false edited-volume expectation fails at its own step. See `PUSH_PULL_EDIT_VERIFICATION_2026-09-08.md`. Both scripts are integrated into the regular workflow test and check script; their final combined Trim gate remains pending.
|
||||
|
||||
## Pattern count correction — after checkpoint 9c297b9
|
||||
|
||||
The selected-pattern count edit had a real silent failure: a second `step` field also wrote count and overwrote the newly typed value on Enter. Removing the misdeclared field makes the existing count setter work. `functions-pattern-count.json` drives actual linear creation, count3→5, undo/redo, invalidcount1 refusal and healthyshrink2, with six analytic one-solid STEP expectations. The release double run passes; a deliberately wrong edited volume fails. Existing pattern-spacing editing is still missing its own command and is not credited as working. See `PATTERN_COUNT_VERIFICATION_2026-09-08.md` for red/green evidence and bounds.
|
||||
|
||||
## Final Trim/function gate and newly reproduced limits
|
||||
|
||||
The combined gate passes 1,411 workspace tests, 24 release Headless tests and 44 CLI selftests. Workspace Clippy and formatting pass. Hole, Push/Pull editing, Pattern Count, both small Trim scripts and legacy Trim/Split pass rebuilt release cross-process checks on llvmpipe; `/tmp/vernier-trim-functions-final-results.json` indexes the six reports. Four false-oracle controls fail at their intended geometry/native assertions, with exit 1. This closes the pending combined gate above, not universal function coverage.
|
||||
|
||||
Trim click and preview now use the same six-pixel radius and Alt override. Actual controls and held-worker tests cover stationary modifiers/camera, popup-painted output, stale reply rejection, exact preview/commit identity and Undo. The two small scripts assert saved topology; the Headless companion's isolated restored-profile extrusion is an explicitly separate STEP oracle. Existing line-profile extrusion after Undo remains unsupported and is not credited by this helper.
|
||||
|
||||
The parameter audit found a new real-control failure outside the explicit-value Push/Pull script: selecting a stored 7.5 mm Push/Pull after creating another at 2 mm and pressing Enter untouched silently replaces the original distance with 2 mm. The export drops from 29400 to 22800 mm³; `/tmp/vernier-parameter-untouched-enter-red/report.json` fails at step 48. Other source findings include same-kind card buffer reuse and rounded text overwriting exact values. These remain open under `PARAMETER_WIRING_AUDIT_2026-09-08.md` and `PARAMETER_CARD_REPAIR_PLAN_2026-09-08.md`; passing explicit-edit tests do not certify untouched or cross-selection behavior.
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Plain-hole real-control verification — 2026-09-08
|
||||
|
||||
## Outcome
|
||||
|
||||
**PASS.** `scripts/drive/functions-hole.json` drives MakeHole and EditHole through the real 1600 × 1000 shell on the release `vernier-drive` binary. It creates two real sketch points on the starter block's top face, locks and Ctrl-selects them, presses the painted Hole command, edits the selected hole through its diameter/depth fields and Through chip, and proves both creation and parameter-edit undo/redo through exported STEP geometry.
|
||||
|
||||
Counterbore is outside this result. The backend carries counterbore state, but the current value card paints no counterbore enable, head-diameter, or head-depth controls; this gate does not claim it.
|
||||
|
||||
## Driven workflow and analytic oracle
|
||||
|
||||
The starter is a 40 × 30 × 15 mm box with volume 18,000 mm³. The script opens a sketch on the named `block` `prism-end` face and uses the actual Point tool at `(12, 15, 15)` and `(28, 15, 15)`. Each point is selected and locked with the painted `▣` control. Ctrl-selecting both produces `two-points`; the actual `ribbon:hole` press creates one hole feature holding both centres.
|
||||
|
||||
Creation uses the shell's 6 mm diameter, Through All defaults. The two bores remove
|
||||
|
||||
`2 * pi * (6/2)^2 * 15 = 270*pi mm³`,
|
||||
|
||||
so the exact expected remainder is
|
||||
|
||||
`18000 - 270*pi = 17151.769983530758 mm³`.
|
||||
|
||||
The script exports and checks this volume at relative tolerance `1e-9`, with exactly one solid. `Ctrl+Z` removes the hole feature, `Ctrl+Y` restores it, and the repeated export has the same analytic volume and the same STEP digest.
|
||||
|
||||
The script then selects `timeline:3`, asserts `feature:hole`, turns off `chip:through`, types diameter `10` into `card:diameter` and depth `6` into `card:depth`, and commits both values with one Enter. The two blind bores remove
|
||||
|
||||
`2 * pi * (10/2)^2 * 6 = 300*pi mm³`,
|
||||
|
||||
leaving
|
||||
|
||||
`18000 - 300*pi = 17057.52220392306 mm³`.
|
||||
|
||||
That STEP also passes at relative tolerance `1e-9` with one solid. Undo restores the Through-All/6 mm result; redo restores the blind/10 × 6 mm result. The undo and redo exports repeat the corresponding STEP digests exactly. The script ends with no compile warnings.
|
||||
|
||||
## Evidence
|
||||
|
||||
Command:
|
||||
|
||||
```text
|
||||
/home/nilsb/Documents/Projects/VernierCAD/target/release/vernier-drive scripts/drive/functions-hole.json --out /tmp/vernier-hole-pass1c --require-adapter llvmpipe --skip-png
|
||||
```
|
||||
|
||||
Result: exit 0; `llvmpipe (LLVM 22.1.8, 256 bits)`; 95 steps; 240 frames; deterministic cross-process execution with two child PIDs. Report: `/tmp/vernier-hole-pass1c/report.json`.
|
||||
|
||||
The artifact pairs prove exact repeatability:
|
||||
|
||||
- Through creation and creation redo: 24,901 bytes, FNV-1a `0x747595647e47b0cf`.
|
||||
- Blind edit and edit redo: 23,006 bytes, FNV-1a `0x005941d22abb3892`.
|
||||
- Undo of the edit returns to the Through artifact: 24,901 bytes, FNV-1a `0x747595647e47b0cf`.
|
||||
|
||||
The first authoring run reached the point selection and showed that Lock is a bespoke painted `▣` control rather than a `ribbon:lock` target. The second reached the completed hole and established the real face-sketch default label `face sketch`. Both were script expectation corrections; neither required a production change.
|
||||
|
||||
## False-oracle control
|
||||
|
||||
A temporary copy at `/tmp/vernier-hole-wrong.json` changes only the first Through-All expected volume from `17151.769983530758` to `17152.769983530758` mm³. Running it with the same release binary and llvmpipe command exits 1 at step 48, its first geometry expectation:
|
||||
|
||||
```text
|
||||
volume: expected 17152.769983530758, got 17151.76998353073 (rel 5.830e-5 > 1e-9)
|
||||
```
|
||||
|
||||
Log: `/tmp/vernier-hole-wrong.log`. Failure report and frozen diagnostic state: `/tmp/vernier-hole-wrong-out/report.json` and `/tmp/vernier-hole-wrong-out/run1/failure/`.
|
||||
|
||||
## Boundaries
|
||||
|
||||
The geometry-driving controls are real clicks and typed input. `export_step`/`expect_step` are used only as the measured oracle. No helper action dispatch creates or edits the hole. The result covers a two-centre plain cylindrical hole, Through All and To Depth, selected-feature re-dimensioning, and genuine keyboard undo/redo. It does not cover counterbore, multi-body target refusal, stale point refusal, or the 62-point cap.
|
||||
@@ -141,3 +141,25 @@ No unsupported adapter may silently stand in for RADV golden verification. Repor
|
||||
- All three release file/Save scripts pass cross-process with deterministic output on llvmpipe; no RADV PNG result is claimed. No live native hang containment is implied by these passing bounded tests.
|
||||
|
||||
- Independent final review is closed for both Save and raw-reference admission; see `FILE_ONLY_SAVE_REVIEW_2026-09-08.md` and `RAW_REFERENCE_ADMISSION_REVIEW_2026-09-08.md`. All implementation writers are frozen for the branch checkpoint. Main integration and new app dependency edges still await the previously requested approvals.
|
||||
|
||||
### Next function and sketch-tolerance pass
|
||||
|
||||
- Previous verified Save/admission batch committed as `9c297b90a9d5b2b05b7b3563528ae3590487b8ef` with an explicit 33-file manifest and committed-blob checks. Main stays at `b3f661f`; user-owned AGENTS.md stays untouched.
|
||||
- Plain-hole MakeHole/EditHole and existing push/pull distance controls now have release actual-control scripts with analytic STEP geometry, undo/redo and intended false-oracle failures. They are integrated for the next combined gate. Counterbore remains missing its user controls.
|
||||
- R4 correction to the earlier ledger: **Trim** retains a fixed 2 mm snap; Split has no snapping, and drag sends the raw target to the solver. The separate point-cross pick geometry is ±1 mm and expands on screen with zoom. Root reproduced small Trim refusal at the middle of a 1 mm edge while the identical Split control passed. `SKETCH_TOLERANCE_FOLLOWUP_2026-09-08.md` records these facts and the independent hover-path findings. Trim tolerance and shared preview correction are in progress; no completion claim yet.
|
||||
|
||||
- New actual-control P2: SetPatternCount's misdeclared millimetre `step` field wrote the same shell count as its real count field, overwriting a typed 3 → 5 edit with the old value. The volume probe failed at 27000 versus 36000 despite a success readout. The count-only card now declares only count; existing spacing editing remains a separate missing command. `functions-pattern-count.json` passes its count/edit/history/refusal/recovery volumes in a release double run. See `PATTERN_COUNT_VERIFICATION_2026-09-08.md`.
|
||||
- Intermediate combined release build passes all three new function scripts on llvmpipe: Hole95steps, PushPull65steps (initial-state and exact-refusal assertions hardened after review), Pattern67steps. Reports: `/tmp/vernier-functions-next-<script>/report.json`. This is not the final frozen Trim/workspace gate. All three are in the regular workflow list and check script.
|
||||
|
||||
- Trim verification surfaced a separate R7 workflow gap: actual Undo restores a line-based rectangle's exact document geometry but clears the active chain, and the GUI cannot then offer extrusion of that existing profile. `extend_chain` is reached only from drawing; `edit_for(Extrude)` accepts the active `closed_profile` or a picked circle, with no picked closed line-chain fallback. This must be fixed as profile selection/reopening work. The bounded Trim Undo geometry check may extrude an isolated restored document as an explicitly declared oracle; it must not claim that GUI extrusion-after-Undo works.
|
||||
|
||||
- The 27-command numeric wiring audit found selected-feature seeding and same-action buffer-cache defects. A real-control reproduction confirms one: create Push/Pull 7.5 mm, create another at 2 mm, select the first and press Enter untouched. Its distance silently becomes 2 mm and the analytic export falls from 29400 to 22800 mm³. `/tmp/vernier-parameter-untouched-enter-red/report.json` fails at its intended final geometry assertion, step 48. This existing defect is a next-batch repair, not closed by the passing explicit-value Push/Pull script. See `PARAMETER_WIRING_AUDIT_2026-09-08.md` for the broader source findings and the exact scope of this reproduction.
|
||||
|
||||
### Combined Trim and function-control gate
|
||||
|
||||
- Trim now uses the drawing gesture's six-pixel radius with Alt override. Hover and commit share the same request, and shared desktop/Headless frame handling rejects stale answers and suppresses popup-covered paint. Independent review closed both the introduced foreign-point precedence defect and the same-frame stale-paint defect. See `TRIM_TOLERANCE_IMPLEMENTATION_2026-09-08.md` and `TRIM_SCREEN_TOLERANCE_REVIEW_2026-09-08.md`.
|
||||
- `/tmp/vernier-trim-functions-workspace.log`: exit 0, **1,411 tests across 66 suites**, 19 ignored fixture/measurement cases, zero failures. Includes twenty modeling workflows, a separate two-zoom Trim/native-topology test, and both ladder positive/negative gates. The only later source change was whitespace formatting of the new driver assertion.
|
||||
- `/tmp/vernier-trim-functions-headless-release.log`: **24/24** release Headless tests passed. `/tmp/vernier-trim-functions-selftest.json`: **44/44** release CLI checks passed. Workspace/all-targets Clippy with warnings denied, workspace formatting and diff whitespace checks passed.
|
||||
- `/tmp/vernier-trim-functions-final-results.json`: Hole, Push/Pull editing, Pattern Count, small Trim at distances 40 and 80, and the revised legacy Trim/Split script all passed on the rebuilt release binary in distinct processes with deterministic output. Reports are `/tmp/vernier-trim-functions-final-<script>/report.json`. All runs pin llvmpipe and skip PNG comparison; no RADV visual certification is claimed.
|
||||
- `/tmp/vernier-trim-functions-negative-results.json`: deliberately wrong Hole, Push/Pull, Pattern and Trim-native assertions all failed with exit 1 at exactly the altered assertion (steps 48, 16, 19 and 25). Existing edited-volume false-oracle controls remain recorded in the individual reports.
|
||||
- The two small Trim scripts assert native topology and actual Undo. The real Headless companion exports/imports an isolated restored-profile extrusion to validate geometry; this explicitly does not close the GUI profile-reopening gap. `PARAMETER_CARD_REPAIR_PLAN_2026-09-08.md` supplies the next repair's exact-value/history requirements. All writers are frozen for this checkpoint; the full goal, native process containment, main integration and previously requested dependency approvals remain open.
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Parameter card repair plan — 2026-09-08
|
||||
|
||||
Read-only requirements review of `PARAMETER_WIRING_AUDIT_2026-09-08.md` and current sources at checkpoint `9c297b9` with the frozen Trim batch in the working tree. No production/test edits or Cargo commands. This plan repairs existing behavior without new features, dependencies or approval flow.
|
||||
|
||||
## Required outcome
|
||||
|
||||
Selecting an existing parameter and pressing Enter without making a change must preserve its exact stored value, document session JSON, allocator, undo/redo, dirty state and geometry. A real creation action must still execute on Enter with its defaults. A document-affecting chip change must still execute even when no numeric text was edited.
|
||||
|
||||
Correct selection seeding alone cannot meet this contract. `shell/value_card.rs:463` seeds text through `format_param` (`:485`): count has zero decimals, degrees one, millimetres and ratios two. Numeric Enter (`:792`) then parses and writes **every** buffer before dispatch. A correctly seeded 1.234567 mm still becomes 1.23; editing one hole field can round its untouched sibling. `EditSketchDimension` converts angle radians to degrees but its generic descriptor is mm, so its actual seeded degree text receives two decimals. Other degree cards receive one. Display/parse inverse functions do not undo decimal rounding; degree/radian arithmetic itself is also not an exact identity for every float.
|
||||
|
||||
Even when the number survives, `vernier-doc/src/document.rs:1600` runs the command on a clone, pushes the old state and clears redo after every successful `Command::apply`. `execute_all` similarly records a nonempty command group. Existing `SetSweepScale` always executes both scale and orientation commands as one group (`vernier-app/src/server/apply_features.rs:523`). Preventing an unchanged edit must happen before this history boundary. Do not alter generic command/history semantics or run-and-Undo the no-op: either can allocate IDs or destroy redo before detecting it.
|
||||
|
||||
## Smallest coherent repair
|
||||
|
||||
1. **Supply exact typed source values.** Extend the existing timeline parameter snapshot with extrude height, revolve angle, sweep scale and orientation, push/pull distance and pattern count. Complete circle/arc radius data in `SketchCurveRow`. Keep the existing richer-fillet refusal; never reconstruct a rich spec from its display scalar. Read the editable constraint target where a radius constraint exists, with an explicit unconstrained-radius fallback. Do not parse timeline labels. Preserve canonical radians separately from display degrees where needed; `FeatureScalar::Draft` currently carries only converted degrees (`shell/mod.rs:126`).
|
||||
2. **Give the draft an owner and a baseline.** Extend `CardState` (`value_card.rs:17`) with a subject key, exact source/candidate values, per-field edit state and document-affecting chip baseline. The key includes action and actual selected identity, not `Selection::Feature(kind)` alone: feature ID; sketch/curve ID; point ID; dimension identity including sketch, slot and constraint kind/references; creation selection/session identity as applicable. Opening another document must invalidate the entire draft even if raw IDs coincide. Presentation mode such as radius/diameter is separate from subject identity.
|
||||
3. **Keep formatted text as presentation.** Untouched buffers use their exact source value, not a parse of their rounded seed. Track actual TextEdit changes per field (`field.changed()` or an equivalent event-based mark); string equality to the seed is insufficient when the user intentionally replaces a rounded seed with the same visible digits. Parse, convert, validate and clamp only edited fields into a temporary candidate; retain exact untouched siblings. Validate all edited fields before changing shared shell values or dispatching. Invalid/nonfinite text leaves the document/history unchanged. Existing range policy can remain, but untouched stored values must not be silently clamped merely because a card rendered them.
|
||||
4. **Decide by the effective command.** Compare the complete candidate with the current exact source for existing edits before calling `session_click`/dispatch. An unchanged candidate produces no document action and no error. A changed candidate commits once; its successful acknowledgement establishes the new baseline. A refusal must not promote the failed candidate to an acknowledged source. Preserve the user's draft for correction while displaying the refusal. Do not mark every card Enter as an edit merely because a TextEdit once changed: typing an exact equivalent value still has no model effect.
|
||||
5. **Keep execution policy distinct from seed metadata.** Creation operations and explicit read-only requests such as Section must retain Enter with unchanged defaults. A new Lock at the currently displayed point position creates a constraint even though its coordinates equal the seed; it cannot be rejected by a generic numeric-equality rule. Define the existing-edit versus creation/request cases explicitly. `Seed::FromFeature` is a source rule, not a reliable edit classification; several creation rows currently misuse it. Correct those rows to `Seed::Last` and require a checked typed source for every remaining `FromFeature` field.
|
||||
|
||||
A shared pure preparation function returning unchanged, invalid or a concrete candidate is preferable to copying these decisions across UI arms. Keep the existing `Edit`/server operations where they already express the complete change. Review the direct ribbon route too: `arms_the_card` (`value_card.rs:119`) deliberately dispatches a click on an already visible card action, and `shell/ribbon.rs:818` can bypass numeric-buffer commit. The same existing-feature equality/source rule must cover that route without making missing/invalid targets appear as successful no-ops. A narrow existing-setter admission check is an alternative backstop; generic `Document::execute` deduplication is not this repair.
|
||||
|
||||
## Chip and default requirements
|
||||
|
||||
`value_card.rs:667-716` contains distinct cases:
|
||||
|
||||
| Chip | Required behavior |
|
||||
| --- | --- |
|
||||
| Existing fillet/chamfer mode | A mode-only change commits one spec edit, retaining the exact untouched radius/distance. Switching back before commit is unchanged. |
|
||||
| Hole through/depth | A through-only change commits the depth-rule change with exact diameter and retained depth/head data. A dormant depth value while ThroughAll stays active may update a last-used draft, but must not manufacture a document edit with an identical effective spec. |
|
||||
| Sweep keep-orientation | Chip-only Enter commits orientation and retains exact scale; one Undo restores both when both were changed. |
|
||||
| Extrude cut | Creation still executes on Enter even when all numbers retain defaults. Existing extrusion acknowledgement/reset behavior remains intact. |
|
||||
| Radius/diameter | Display-only change never creates history. Never reinterpret an uncommitted radius string as a diameter. Transform a valid draft using its previous mode, preserving its exact candidate; retain or reject an invalid draft locally without committing it. |
|
||||
| Section cutaway/flip | Existing view behavior remains available. Do not turn these into document mutations or suppress an explicit Section request merely because the offset is unchanged. |
|
||||
|
||||
Chips currently mutate shared shell fields immediately, while Escape only disarms the text card (`value_card.rs:756`). Keep document-affecting chip choices in the owned draft or restore their baseline on cancellation/subject change; otherwise a canceled chip can leak into a later edit. Display preferences may persist as preferences. Separate sketch radius, edge-fillet radius and chamfer distance defaults so editing one domain does not silently change another creation default; retain deliberate sharing only where the product already intends it.
|
||||
|
||||
## Selection and scene lifecycle
|
||||
|
||||
- **A → B, same action/kind:** discard A's draft and arm state, seed B from its typed source before displaying or dispatching. This includes point picks, circle/arc picks, dimension picks, and Ctrl-selection transitions that leave a single remaining subject. Do not depend on a frame with no visible card occurring between selections.
|
||||
- **Same subject, unrelated scene:** keep draft text, focus, caret and chip edits. Background hover, Save or a recompute with an unchanged source must not reseed text. A blanket disarm inside `apply_scene` would break typing now that Trim can submit background scenes.
|
||||
- **Same subject, source changed by Undo/Redo or acknowledgement:** refresh the exact source and untouched fields. Preserve actively edited fields as explicit user drafts against the current subject; compare them with the refreshed baseline on commit. Refresh dimension anti-stale tokens only while the same underlying constraint still exists. A changed constraint kind/referenced geometry at a reused index is a subject change, not permission to retarget old text.
|
||||
- **Subject removed, selection cleared, document opened, action unavailable:** discard the old draft; it must not revive later on another object with the same action or raw numeric ID.
|
||||
- **Cancel:** no document edit or history change; restore staged document-affecting chips and numbers. A later creation may retain approved last-used defaults only from its intended state, not another subject's canceled edit.
|
||||
|
||||
Current selection seeds live in `toggle_selection` (`shell/mod.rs:2110`), point picking (`vernier-app/src/picking.rs:298`), and `pick_dimension`/`refresh_dimension_pick` (`shell/mod.rs:2643`, `:2678`). The last function already refreshes a dimension token/source only when its value moved, but does not provide per-field buffer ownership. Use one shared reconciliation rule instead of fixing only timeline clicks.
|
||||
|
||||
## Regression requirements
|
||||
|
||||
Tests must exercise the real selection/card/Enter path, not initialize shell fields directly. Use these parallel checks:
|
||||
|
||||
- For every existing numeric setter, select a non-default value such as 1.23456789 mm or ratio, and a non-round angle in canonical radians. Press untouched Enter; assert exact session JSON including both histories/allocator, unchanged dirty flag, and no command submission. Build a redo branch beforehand, then prove actual Ctrl+Y still restores it. Include 4 as a count: even a losslessly formatted value currently destroys redo.
|
||||
- For each same-action subject family, show A, select B without an empty frame, then untouched Enter. Repeat with a dirty A buffer. B must show its source and remain exact. Include dimensions whose values change at the same slot and a removed/replaced constraint.
|
||||
- Edit only one field of a multi-field card, especially EditHole and Lock. Untouched siblings retain exact bits; an invalid edited sibling prevents partial writes. Test negative/small values within legitimate domains and angle/diameter conversions.
|
||||
- Toggle each document-affecting chip with all numeric fields untouched; assert exactly the intended payload change and one undo step. Toggle back and assert no edit. Diameter display-only toggles and repeated Enter preserve exact radius/history. Escape restores document-affecting draft choices.
|
||||
- Create through actual Enter using untouched Extrude/Fillet/Hole defaults, and create a Lock at its seeded position. This is the positive control against an overbroad no-change gate. Section still runs as an explicit read-only action.
|
||||
- While text is partially typed, deliver unrelated hover/Save/recompute scenes; preserve the buffer/caret. Then Undo/Redo or acknowledge a changed source on that same subject: clean fields refresh, dirty fields survive without silently adopting another subject. Test invalid partial text such as `-` as well as valid numbers.
|
||||
- Exercise ribbon re-activation of the already visible action, failed command acknowledgement, successful edit followed by another untouched Enter, and open-with-reused-IDs. Verify the same no-change/ownership contract at each entry.
|
||||
- Add registry-driven coverage of every `FromFeature` field with sentinel shell defaults, requiring the typed source and expected `param_value`; assert all keys remain distinct. Retain richer-feature refusal tests and cross-domain default-isolation checks.
|
||||
|
||||
Driven geometry exports are useful alongside exact native/session assertions, but geometry equality alone cannot expose redo loss or one-ULP changes. Preserve the original wiring audit as historical evidence and append the final tested disposition after implementation. This plan itself claims source-backed requirements only, not executed regressions.
|
||||
|
||||
Plan frozen on 2026-09-08.
|
||||
@@ -0,0 +1,205 @@
|
||||
# Registered numeric parameter wiring audit — 2026-09-08
|
||||
|
||||
## Scope and method
|
||||
|
||||
This is a read-only source audit of every non-empty `CommandDescriptor::params` entry in
|
||||
`crates/vernier-ui/src/registry.rs`. For each field I traced the descriptor key and unit through
|
||||
`shell/value_card.rs::{param_value,set_param,card_display,card_parse}`, then through the action's
|
||||
`edit_for` or app-only consumer. I also traced selection-time seeding because Enter parses and
|
||||
writes **every** visible buffer before dispatch (`shell/value_card.rs:792-816`); an untouched or
|
||||
stale field is therefore a document edit, not inert presentation state.
|
||||
|
||||
No GUI script or Cargo test was run for this audit, and this report does not claim driven
|
||||
verification.
|
||||
|
||||
## Actionable findings
|
||||
|
||||
### 1. Five existing-feature numeric cards do not seed from the selected feature
|
||||
|
||||
`SetExtrudeHeight`, `SetRevolveAngle`, `SetSweepScale`, `SetPushPullDistance`, and
|
||||
`SetPatternCount` all declare `Seed::FromFeature` (`registry.rs:1191-1301`). Their dispatches read
|
||||
the corresponding plain shell fields (`edit.rs:1445-1456`, `1973-1991`). However, a timeline row
|
||||
only carries a hole spec and three scalar variants: fillet/chamfer, shell, and draft
|
||||
(`server/view.rs:503-579`). `toggle_selection` can consequently seed only those same values and a
|
||||
hole (`shell/mod.rs:2143-2185`). It has no branch or data for extrude height, revolve angle, sweep
|
||||
scale/orientation, push/pull distance, or pattern count.
|
||||
|
||||
The result is a silent edit on untouched Enter. For example, after opening a document the shell
|
||||
defaults are height 15, revolve 360, sweep scale 1, push/pull 2, and pattern count 4
|
||||
(`shell/mod.rs:1774-1795`). Selecting a stored 7.5 mm push/pull leaves `distance_mm == 2`; its card
|
||||
shows 2.00, and Enter dispatches `SetPushPullDistance { distance: 2.0 }`. The same mechanism applies
|
||||
to the other four setters. Existing focused tests construct the desired shell value directly;
|
||||
they do not prove selection-to-card seeding.
|
||||
|
||||
Smallest correction: extend the row's typed parameter snapshot (the existing `FeatureScalar`
|
||||
seam, or a replacement keyed by setter action) with these five payload values, including
|
||||
`Sweep::keep_orientation`, and seed them in `toggle_selection`. Add one sentinel test per setter:
|
||||
start shell state at a different value, select the feature, render the card, press Enter without
|
||||
typing, and assert exact session JSON and exported geometry are unchanged.
|
||||
|
||||
### 2. Picking a circle or arc does not seed the advertised radius card
|
||||
|
||||
`EditDimension.radius` is explicitly `Seed::FromFeature` (`registry.rs:794-820`) and dispatches
|
||||
`SetSketchRadius` from `shell.radius_mm` (`edit.rs:1745-1760`). `param_value`/`set_param` map it to
|
||||
that field (`value_card.rs:243,322`). But `SketchCurveRow` carries only kind, label, construction,
|
||||
and defining points (`shell/mod.rs:1315-1345`); `pick_curve` only changes ids
|
||||
(`shell/mod.rs:2570-2622`), and the app's curve-pick route adds no numeric seed
|
||||
(`picking.rs:265`). A circle's authored radius is only embedded in a display string while the arc
|
||||
radius may live in a `Radius` constraint (`server/view.rs:273-320`).
|
||||
|
||||
Thus a newly selected circle/arc radius card shows whichever `radius_mm` another operation left
|
||||
behind (default 1.00), and untouched Enter can resize the curve. Carry the effective editable
|
||||
radius as typed data in `SketchCurveRow` (authored circle radius or the existing radius constraint;
|
||||
derived arc radius where unconstrained), seed it on a non-toggle curve pick, and add a real-card
|
||||
test with a non-default circle and arc.
|
||||
|
||||
### 3. The card buffer cache does not include the selected subject
|
||||
|
||||
Even the currently seeded fillet, shell, draft, and hole fields can display stale digits when the
|
||||
user switches directly between two features whose cards use the same action. `toggle_selection`
|
||||
updates the numeric shell fields (`shell/mod.rs:2143-2185`) but does not clear `CardState`.
|
||||
`value_card` re-seeds only when the **action**, field count, or diameter mode changes
|
||||
(`value_card.rs:525-535`). Two hole rows both show `EditHole`; two shell rows both show
|
||||
`SetShellThickness`; the cached buffers therefore survive the selection change. Enter parses the
|
||||
old buffer back over the correctly seeded shell field and dispatches it (`value_card.rs:792-816`).
|
||||
|
||||
Smallest correction: invalidate/disarm the card whenever its selection subject changes, before
|
||||
the next frame seeds buffers from the new subject. A more explicit alternative is to add the
|
||||
selected feature/curve/dimension identity to `CardState`'s seed key. Test two different values of
|
||||
each same-action type by selecting A, rendering, selecting B without closing the card, and pressing
|
||||
Enter untouched; B must remain exact.
|
||||
|
||||
### 4. `radius_mm` aliases unrelated creation defaults
|
||||
|
||||
The same `ShellState::radius_mm` backs sketch circle/arc radius editing, fillet creation, chamfer
|
||||
creation, and fillet/chamfer re-dimensioning (`value_card.rs:224,243,272-274,322`). The selected
|
||||
fillet path overwrites the field with its own value, but a picked body edge has no feature value to
|
||||
seed. Consequently editing a 25 mm sketch circle can make the next untouched edge-fillet card
|
||||
propose 25 mm, and fillet and chamfer also overwrite one another's last-used defaults despite
|
||||
having different labels (`radius` versus `distance`, `registry.rs:540-562`).
|
||||
|
||||
This does not produce the old multi-buffer overwrite in one Enter, but it is a wrong default alias.
|
||||
Give sketch radius, fillet radius, and chamfer distance separate last-used state (or define and test
|
||||
an explicit shared-default policy). `SetFilletSpec` may continue to seed the appropriate field from
|
||||
the selected feature.
|
||||
|
||||
### 5. `Seed` is descriptive metadata rather than an enforced wiring rule
|
||||
|
||||
`ParamSpec::seed` is never read by production value-card code; `seed_buffers` always calls
|
||||
`param_value` (`value_card.rs:463-481`). This allowed findings 1 and 2 despite the descriptors
|
||||
claiming `FromFeature`. It also makes several creation rows misleading: Extrude, MakeHole,
|
||||
FilletEdge, ChamferEdge, Shell, Pattern, and PatternCircular declare `FromFeature` even though they
|
||||
create a new operation and their numeric fields come from shell defaults/previous use
|
||||
(`registry.rs:395-405,498-510,540-682`).
|
||||
|
||||
After correcting those creation rows to `Seed::Last`, add a registry-driven invariant for every
|
||||
remaining `FromFeature` field. The test should require a typed scene value and prove that selecting
|
||||
its subject changes `param_value` away from a sentinel. This turns `Seed` into a checked contract
|
||||
instead of a comment that can drift.
|
||||
|
||||
## Complete numeric inventory
|
||||
|
||||
The table covers all 27 registered commands with numeric params. “Consumed” means the value
|
||||
reaches the action's edit/app payload; it does not mean the selection-time seed is correct.
|
||||
|
||||
| Registered action | Keys and declared units | `ShellState` storage | Consumer and conversion | Audit result |
|
||||
|---|---|---|---|---|
|
||||
| `Extrude` | `height` mm | `extrude_height_mm` | `ExtrudeSketch.height` | Consumed; creation seed metadata should be `Last` |
|
||||
| `PushPull` | `distance` mm, signed | `distance_mm` | `PushPull.distance` | Consumed |
|
||||
| `Draft` | `angle` deg, signed | `angle_deg` | `Draft.angle`, degrees to radians | Consumed |
|
||||
| `MakeHole` | `diameter`, `depth` mm | `hole_diameter_mm`, `hole_depth_mm` | `MakeHole`; depth enters `ToDepth` only when Through is off | Consumed conditionally as designed; creation seed metadata should be `Last` |
|
||||
| `FilletEdge` | `radius` mm | `radius_mm` | `fillet_spec_from(..., false)` | Consumed; wrong cross-domain default alias |
|
||||
| `ChamferEdge` | `distance` mm | `radius_mm` | `fillet_spec_from(..., true)` | Consumed; wrong cross-domain default alias |
|
||||
| `Shell` | `thickness` mm | `thickness_mm` | `Shell.thickness` | Consumed; creation seed metadata should be `Last` |
|
||||
| `Pattern` | `count` count; `dx`,`dy` mm; `count2` count; `dx2`,`dy2` mm | six distinct pattern fields | `Pattern`; second axis is present only when `count2 > 1` | All six keys distinct and consumed; creation seed metadata should be `Last` |
|
||||
| `PatternCircular` | `count` count; `cx`,`cy` mm; `span` deg | four distinct pattern fields | `PatternCircular`, span degrees to radians | All four keys distinct and consumed; creation seed metadata should be `Last` |
|
||||
| `Section` | `offset` mm, signed | `section_offset_mm` | `Section.offset_mm` and cutaway view | Consumed |
|
||||
| `ConstrainLength` | `length` mm | `length_mm` | `ConstrainLength.mm` | Consumed |
|
||||
| `ConstrainAngle` | `angle` deg | `sketch_angle_deg` | `ConstrainAngle.radians`, degrees to radians | Consumed |
|
||||
| `EditDimension` | `radius` mm | `radius_mm` | `SetSketchRadius.radius`; diameter chip display/parse is exact inverse | Consumed; feature seed missing and wrong cross-domain alias |
|
||||
| `ConstrainPointDistance` | `distance` mm | `length_mm` | `PointConstraint::Distance` | Consumed; deliberate last-used dimension value |
|
||||
| `ConstrainLock` | `x`,`y` mm, signed | `lock_x`, `lock_y` | `SetPointLock.at` | Both distinct and consumed; viewport point pick seeds solved coordinates (`picking.rs:298-306`) |
|
||||
| `SketchFillet` | `radius` mm | `sketch_fillet_radius_mm` | `SketchFillet.radius` | Consumed |
|
||||
| `AddLoftSection` | `offset` mm | `loft_section_offset_mm` | app-only `add_loft_section` (`sessions.rs:368-384`) | Consumed |
|
||||
| `SetExtrudeHeight` | `height` mm | `extrude_height_mm` | `SetExtrudeHeight.height` | Consumed; selected-feature seed missing |
|
||||
| `SetRevolveAngle` | `sweep` deg | `revolve_angle_deg` | `SetRevolveAngle.angle`, degrees to radians | Consumed; selected-feature seed missing |
|
||||
| `SetSweepScale` | `scale` ratio | `sweep_scale_end` | `SetSweepScale.scale_end` | Consumed; scale and orientation seed missing |
|
||||
| `SetFilletSpec` | `radius` mm | `radius_mm` | constant fillet/equal chamfer spec | Consumed and seeded for simple specs; richer specs correctly refused |
|
||||
| `SetShellThickness` | `thickness` mm | `thickness_mm` | `SetShellThickness.thickness` | Consumed and underlying state seeded; same-action buffer risk remains |
|
||||
| `SetDraftAngle` | `angle` deg, signed | `angle_deg` | `SetDraftAngle.angle`, degrees to radians | Consumed and underlying state seeded; same-action buffer risk remains |
|
||||
| `SetPushPullDistance` | `distance` mm, signed | `distance_mm` | `SetPushPullDistance.distance` | Consumed; selected-feature seed missing |
|
||||
| `SetPatternCount` | `count` count | `count` | `SetPatternCount.count` | One field, consumed; selected-feature seed missing |
|
||||
| `EditHole` | `diameter`, `depth` mm | separate hole fields | `SetHoleSpec`; depth enters `ToDepth` only when Through is off | Both mapped distinctly; state seeded; same-action buffer risk remains |
|
||||
| `EditSketchDimension` | generic `value` (descriptor mm) | `sketch_dimension_value` | `SetSketchDimension.value`; angle display/parse converts radians/degrees dynamically | Consumed and seeded by the picked dimension token |
|
||||
|
||||
The common mapping functions are at `value_card.rs:215-326`. Action-specific creation and setter
|
||||
consumers are at `edit.rs:1416-1506, 1508-1537, 1680-1773, 1881-1934, 1973-1995, 2054-2062`;
|
||||
`AddLoftSection` is intercepted by `app.rs:1491-1497` and consumes its offset in
|
||||
`sessions.rs:368-384`.
|
||||
|
||||
## Deliberate conditional and richer-model cases
|
||||
|
||||
- Hole `depth` is visible even when Through is on. It is retained for the next To Depth choice,
|
||||
while `hole_depth_of` correctly emits `ThroughAll` (`edit.rs:2152-2165`). This is a conditional
|
||||
field, not an unconditional dead wire. The card would be clearer if it disabled the depth input
|
||||
while Through is active, but that is presentation work rather than lost dispatch data.
|
||||
- Linear pattern `dx2`/`dy2` are retained while `count2 == 1`; the payload intentionally omits the
|
||||
second axis until `count2 > 1` (`edit.rs:1914-1927`). The registry documents this model at
|
||||
`registry.rs:587-593`.
|
||||
- Hole counterbore head diameter/depth exist in shell/document state and are consumed when the
|
||||
counterbore chip is on (`edit.rs:2168-2173`), but they are not registered numeric params. That is
|
||||
unexposed richer backend state, not a broken advertised numeric field.
|
||||
- Variable-radius fillets and asymmetric/distance-angle chamfers carry more values than
|
||||
`SetFilletSpec` can express. `server/view.rs:527-566` marks them richer and
|
||||
`edit.rs:1416-1430` refuses the flattening edit. This is safe refusal, not a missing consumer.
|
||||
- `EditSketchDimension` declares one generic mm field because distance, radius, and angle share one
|
||||
row. Its dynamic angle label and radians/degrees inverse at `value_card.rs:392-434` are coherent;
|
||||
no unit defect was found.
|
||||
|
||||
## Duplicate and dead-field conclusion
|
||||
|
||||
After removal of SetPatternCount's bogus `step` field, no registered command has two parameter
|
||||
keys mapping to the same state slot, and no advertised numeric field is unconditionally written
|
||||
and then ignored by its dispatched action. The remaining correctness defects are selection
|
||||
seeding, subject-insensitive buffer caching, and the cross-domain `radius_mm` default alias above.
|
||||
|
||||
## Driven reproduction: untouched Push/Pull Enter is destructive
|
||||
|
||||
The missing selected-feature seed in finding 1 was reproduced through the release
|
||||
`vernier-drive` binary and real painted controls, with no production or committed-script change.
|
||||
|
||||
- Temporary script: `/tmp/vernier-parameter-untouched-enter-red.json`
|
||||
- Report: `/tmp/vernier-parameter-untouched-enter-red/report.json`
|
||||
- Failure bundle:
|
||||
`/tmp/vernier-parameter-untouched-enter-red/run1/failure/{document.json,shell.txt,step.txt,frame.png,texts.txt,view.txt}`
|
||||
- Command:
|
||||
`/home/nilsb/Documents/Projects/VernierCAD/target/release/vernier-drive
|
||||
/tmp/vernier-parameter-untouched-enter-red.json --out
|
||||
/tmp/vernier-parameter-untouched-enter-red --once --skip-png --require-adapter llvmpipe`
|
||||
- Result: exit 1 at step 48, the final `expect_step`, with
|
||||
`expected 29400, got 22800 (rel 2.245e-1 > 1e-9)`. The report identifies
|
||||
`llvmpipe (LLVM 22.1.8, 256 bits)`.
|
||||
|
||||
The script starts from the standard 40 × 30 × 15 mm block. It clicks the named top face, types a
|
||||
5 mm Push/Pull in the painted distance card, selects that timeline feature, and edits it to
|
||||
7.5 mm. An intermediate STEP export passes at
|
||||
`40 × 30 × (15 + 7.5) = 27000 mm³` with six faces and one solid, tolerance `1e-9`.
|
||||
|
||||
It then clicks the moved top face and creates a second 2 mm Push/Pull through the same painted
|
||||
card. The precondition STEP export passes at
|
||||
`40 × 30 × (15 + 7.5 + 2) = 29400 mm³`, again six faces and one solid at `1e-9`. This real second
|
||||
operation is what changes `ShellState.distance_mm` to 2.0 without changing the first feature's
|
||||
stored 7.5 mm intent.
|
||||
|
||||
Finally the script selects `timeline:2`, the first Push/Pull, focuses `card:distance`, and presses
|
||||
Enter without selecting or typing any digits. There is no error and feature count remains four,
|
||||
but the exported volume becomes
|
||||
`40 × 30 × (15 + 2 + 2) = 22800 mm³`. The failure document confirms both Push/Pull payloads now
|
||||
store `distance: 2.0`; the shell bundle confirms the selected first feature, `distance_mm: 2.0`,
|
||||
and card buffer `"2.00"`. Thus the failure is the advertised untouched-Enter mutation, not a pick,
|
||||
compile, export, face-count, or tolerance failure.
|
||||
|
||||
This was one bounded `--once` execution. It establishes the control-flow defect and analytic
|
||||
geometry change but does not claim cross-process determinism, PNG comparison, coverage of the
|
||||
same-action A-to-B buffer-cache defect, or coverage of the analogous Extrude/Revolve/Sweep/Pattern
|
||||
and sketch-radius cases.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Pattern-count card correction review — 2026-09-08
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS, no blocking finding.** The one-file production change removes a UI field that had no corresponding `SetPatternCount` input and was demonstrably overwriting the typed count. The real-control drive is red on checkpoint `9c297b9`, green on the updated release binary, distinguishes every intended state with analytic exported geometry, and has a working false-oracle control.
|
||||
|
||||
This review is read-only. I did not modify production, scripts, or tests and did not start a build or drive process.
|
||||
|
||||
## Defect and correction
|
||||
|
||||
Before the change, the selected-pattern card declared two parameters: `count` and a millimetre `step`. The value-card mapping sends **every** `SetPatternCount` parameter to the same `ShellState::count` field (`crates/vernier-ui/src/shell/value_card.rs:229`, `crates/vernier-ui/src/shell/value_card.rs:279-289`). Enter parses and applies all buffers in descriptor order. Typing 5 into `count` therefore set count to 5, then the untouched second buffer, seeded as 3, set it back to 3.
|
||||
|
||||
The diff in `crates/vernier-ui/src/registry.rs:1286-1303` removes `mm("step", Seed::FromFeature)` and leaves one `count` parameter with the existing `1..=64` card range. This matches the document command exactly: `SetPatternCount` accepts only `feature` and `count`, validates the resulting total, and mutates only the payload count (`crates/vernier-doc/src/command/features.rs:1243-1252`, `crates/vernier-doc/src/command/features.rs:1285-1307`). No document format, kernel rule, or spacing value changes.
|
||||
|
||||
The remaining wildcard match for `Action::SetPatternCount` in `param_value`/`set_param` is safe with the corrected single-parameter descriptor. A future second field on this action would recreate the alias, so any spacing implementation must introduce its own action, state field, edit, and document command rather than adding a parameter to this row. The new registry comment says this directly. Existing pattern spacing editing remains missing and is not covered or implied by this fix.
|
||||
|
||||
## Actual-control coverage
|
||||
|
||||
`scripts/drive/functions-pattern-count.json` starts from the two-row starter, selects `timeline:1`, clicks the real `ribbon:pattern`, types creation `count = 3` and `dx = 10` through the aimed creation card, and commits with Enter (`scripts/drive/functions-pattern-count.json:20-83`). It then selects `timeline:2`, asserts `feature:pattern`, and types 5 through the selected feature's actual `card:count` (`scripts/drive/functions-pattern-count.json:100-136`). There is no helper action dispatch; STEP export is used only as the oracle.
|
||||
|
||||
Feature count stays three during every count edit, undo, redo, refusal, and recovery. The trace retains `feature:pattern` through the refused count 1 and resolves the next `card:count` click before the successful shrink to 2. This proves the same pattern feature is edited, the refusal does not destroy its card state, and the next operation remains healthy.
|
||||
|
||||
## Analytic geometry
|
||||
|
||||
The 40 × 30 × 15 mm source is copied along x at 10 mm spacing. Because adjacent 40 mm instances overlap, count `n` unifies into one box whose x length is `40 + (n - 1) * 10`. Therefore
|
||||
|
||||
`V(n) = 30 * 15 * (40 + (n - 1) * 10)`.
|
||||
|
||||
The script checks the three distinct values at relative tolerance `1e-9`, always with one solid and six faces:
|
||||
|
||||
- count 3: `30 * 15 * 60 = 27000 mm³`;
|
||||
- count 5: `30 * 15 * 80 = 36000 mm³`;
|
||||
- count 2: `30 * 15 * 50 = 22500 mm³`.
|
||||
|
||||
These oracles distinguish absolute count replacement from a no-op, an appended feature, or a spacing change. In particular, the old defect reports a successful edit but remains at 27,000 mm³ and fails the 36,000 mm³ expectation.
|
||||
|
||||
## Red, green, history, and refusal evidence
|
||||
|
||||
The pre-fix report `/tmp/vernier-pattern-count-red/report.json` records failure at step 31 on checkpoint release `9c297b9`: expected 36,000, got 27,000. Its `pattern-created.step` and `pattern-edited.step` have the same digest `0x1f93d26eb93b2db7`, proving the successful-looking card commit changed no geometry.
|
||||
|
||||
The updated report `/tmp/vernier-functions-next-functions-pattern-count/report.json` records a pass on `llvmpipe (LLVM 22.1.8, 256 bits)`: 67 steps, 99 frames, deterministic cross-process execution with PIDs 13 and 72. Both child reports contain identical artifact evidence:
|
||||
|
||||
| State | FNV-1a | Meaning |
|
||||
|---|---|---|
|
||||
| creation, undo | `0x1f93d26eb93b2db7` | count 3 / 27,000 mm³ |
|
||||
| edit, redo, refused count 1 | `0xa5a309446ef2003d` | count 5 / 36,000 mm³ |
|
||||
| healthy shrink | `0x4c83ad9abf75b3e3` | count 2 / 22,500 mm³ |
|
||||
|
||||
`Ctrl+Z` and `Ctrl+Y` are real key steps and restore the corresponding geometry exactly. Count 1 is refused with the exact readout `pattern count 1 outside 2..=64`; the export remains byte-identical to count 5. Typing count 2 afterward succeeds and produces the third digest, proving rollback and continued use rather than a merely inert refusal.
|
||||
|
||||
The false-oracle copy changes only the edited volume from 36,000 to 36,001 mm³. It exits 1 at step 32 with actual 36,000 and relative error `2.778e-5 > 1e-9`; evidence is `/tmp/vernier-pattern-count-wrong.log`.
|
||||
|
||||
## Evidence qualification
|
||||
|
||||
`/tmp/vernier-pattern-card-ui.log` shows 46 shell UI tests passing. Those are compatibility evidence, not the regression proof: the same broad suite did not itself expose the old two-buffer overwrite. The checkpoint red run, updated real-control green run, artifact state changes, and false-oracle failure are the evidence that specifically exercises this correction.
|
||||
|
||||
The gate covers overlapping one-direction linear **feature** patterns and first-direction count editing. It does not cover spacing edits, circular or grid count edits, body patterns, or the native hang-containment boundary.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Pattern count card correction — 2026-09-08
|
||||
|
||||
The actual selected-pattern card reported success after count 3 → 5, but the exported volume stayed at the three-instance result. `SetPatternCount` declared a count field and an extra millimetre `step` field. Both fields mapped to `ShellState::count`; Enter parsed both buffers in order, so the unchanged step buffer overwrote the newly typed count. The app and document command only accept a count, not spacing.
|
||||
|
||||
The correction removes the misdeclared step field from this count-only card. It does not add a pattern-spacing setter; editing existing spacing remains a missing function. No document command, format or dependency changed.
|
||||
|
||||
`functions-pattern-count.json` drives actual linear-pattern creation and selected-feature count editing. Three translated starter extrudes at dx = 10 mm overlap into one 60 × 30 × 15 mm block. For count n the union has volume `30 * 15 * (40 + (n-1)*10)`. The script asserts count 3 at 27000 mm³, count 5 at 36000, undo/redo, refused count 1 with unchanged 36000 geometry, and subsequent successful count 2 at 22500. Every STEP requires one six-face solid; feature count stays at three. STEP helpers only measure; no helper creates or changes the pattern.
|
||||
|
||||
Evidence:
|
||||
|
||||
- Before correction: `/tmp/vernier-pattern-count-red.json` against release checkpoint 9c297b9 fails at step 31, expected 36000 but got 27000. The action had reported no error. Report: `/tmp/vernier-pattern-count-red/report.json`.
|
||||
- After correction: `/tmp/vernier-functions-next-functions-pattern-count/report.json` passes 67 steps / 99 frames, deterministic in two processes on llvmpipe. This intermediate build also contains the developing Trim fix; final combined verification remains separate.
|
||||
- `/tmp/vernier-pattern-card-ui.log`: 46 existing shell UI tests pass.
|
||||
- Actual run1 STEP bytes are identical for creation/undo, edit/redo and edit/refusal. These are observed byte comparisons; the script asserts geometry rather than fixed digests.
|
||||
- `/tmp/vernier-pattern-count-wrong.json` changes the edited-body oracle to 36001. It exits 1 at step 32, actual volume 36000; log `/tmp/vernier-pattern-count-wrong.log`.
|
||||
|
||||
The new script is included in the regular workflow test and `scripts/check.fish`. Independent review and final combined gates are recorded separately. This result covers overlapping linear feature instances and first-direction count editing, not circular/grid placement, body patterns, existing spacing editing or native hang containment.
|
||||
@@ -125,3 +125,7 @@ Run relevant app/document/UI unit suites and existing native-file tests after im
|
||||
### File-only worker reply follow-up
|
||||
|
||||
The production desktop and Headless workers now use `ServerReply::{Scene(Box<Scene>), Saved(SaveAcknowledgement)}` through `DocumentServer::reply`. The Save primitive is reached and its staging suppression removed; complete checkpoint capture/restore remain staged. The future codec must carry both reply alternatives and the new `Scene::compile_error`, which distinguishes a broken current document from an edit refusal whose rollback rebuilt successfully. See `FILE_ONLY_SAVE_IMPLEMENTATION_2026-09-08.md`. There is still no byte transport, supervisor or native hang containment.
|
||||
|
||||
### Trim preview request identity follow-up
|
||||
|
||||
The Trim work adds `snap_tolerance_mm: Option<f64>` to both `Edit::TrimCurve` and `Edit::CutHover`; the Edit inventory stays at 69 variants. `None` means optional point snapping is disabled, including Alt, while the geometric own-endpoint refusal still applies. The future codec must preserve this distinction and the exact finite tolerance. `Scene::cut_hover` echoes the complete `CutQuestion` tuple (curve id, point, tool, optional tolerance), so a response requested before a zoom or modifier change cannot paint a stale preview. Cover each echo alternative and tolerance changes in roundtrip fixtures; transporting only the point would discard a correctness condition enforced by the current in-process worker.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Push/pull parameter-edit drive review — 2026-09-08
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS, no blocking finding.** `scripts/drive/functions-push-pull-edit.json` uses the real shell controls for creation and re-dimensioning, proves the same timeline feature is edited rather than appended, and distinguishes undo, redo, refusal rollback, and recovery through analytic STEP geometry. The supplied release report is a real two-process llvmpipe run and both child reports contain identical artifact digests.
|
||||
|
||||
This is an independent read-only review. I did not rerun the script or modify it. Root supplied `/tmp/vernier-push-pull-edit-pass/report.json`; it records exit-pass behavior for 63 steps, 85 frames, PIDs 14 and 73, adapter `llvmpipe (LLVM 22.1.8, 256 bits)`, and `deterministic: true` with `determinism_mode: cross-process`.
|
||||
|
||||
## Control-path review
|
||||
|
||||
- Creation selects the starter `block` face by its persisted `prism-end` name, asserts `faces:1:planar`, clicks the painted `card:distance`, types `5`, and presses Enter (`scripts/drive/functions-push-pull-edit.json:27-71`). This is the face selection's real PushPull card: the live registry row is `Action::PushPull`, label `push/pull`, with one signed `distance` field (`crates/vernier-ui/src/registry.rs:442-460`), and `edit_for` maps the picked face plus that field to `Edit::PushPull` (`crates/vernier-app/src/edit.rs:1881-1884`). There is no driver `action` helper in the script.
|
||||
- The script then clicks `timeline:2`, settles the value-card area, and asserts `feature:pushpull` before typing `7.5` into `card:distance` (`scripts/drive/functions-push-pull-edit.json:89-124`). That selected-feature card is the registry's `SetPushPullDistance` value row (`crates/vernier-ui/src/registry.rs:1271-1284`), and `edit_for` carries the selected feature id into `Edit::SetPushPullDistance` (`crates/vernier-app/src/edit.rs:1445-1451`).
|
||||
- Every later successful and refused edit keeps the feature count at three. The supplied trace keeps selection `feature:pushpull` through undo, redo, zero refusal, and the final `-2` edit. After the refusal, the next `card:distance` target resolves and the `-2` Enter succeeds, so recovery proves the selected card remains usable; an explicit post-refusal selection assertion is not needed to infer that state.
|
||||
- `ctrl+z` and `ctrl+y` are actual key steps (`scripts/drive/functions-push-pull-edit.json:141-197`). STEP export is used only as the geometry oracle.
|
||||
|
||||
## Analytic geometry
|
||||
|
||||
The starter is 40 × 30 × 15 mm. Moving its 40 × 30 top cap by signed distance `d` gives a 40 × 30 × `(15 + d)` box for these three values:
|
||||
|
||||
- Creation `d = 5`: `40 * 30 * 20 = 24000 mm³`.
|
||||
- Edit `d = 7.5`: `40 * 30 * 22.5 = 27000 mm³`.
|
||||
- Recovery edit `d = -2`: `40 * 30 * 13 = 15600 mm³`.
|
||||
|
||||
Each expectation also requires six faces and one solid at relative tolerance `1e-9`. The three volumes are independently derived and distinguish setting an absolute feature distance from adding a delta to the prior edit: treating `-2` as a delta from `7.5` would produce 24,600 mm³ and fail.
|
||||
|
||||
## Artifact and history evidence
|
||||
|
||||
Both `/tmp/vernier-push-pull-edit-pass/run1/report.json` and `run2/report.json` record the same sizes and FNV-1a digests:
|
||||
|
||||
| State | STEP bytes | FNV-1a | Meaning |
|
||||
|---|---:|---|---|
|
||||
| `created.step` | 15,331 | `0x4a6ef5e1afb4cfbd` | 5 mm creation |
|
||||
| `edited.step` | 15,350 | `0xcaec678a5874df26` | 7.5 mm edit |
|
||||
| `undone.step` | 15,331 | `0x4a6ef5e1afb4cfbd` | undo returns exactly to creation |
|
||||
| `redone.step` | 15,350 | `0xcaec678a5874df26` | redo returns exactly to the edit |
|
||||
| `after-refusal.step` | 15,350 | `0xcaec678a5874df26` | refused zero leaves geometry unchanged |
|
||||
| `inward.step` | 15,323 | `0xaf2f2e28071a3299` | valid negative edit continues successfully |
|
||||
|
||||
The zero edit is required to refuse at the document boundary: `SetPushPullDistance` accepts finite signed values but rejects exactly zero before mutating the payload (`crates/vernier-doc/src/command/features.rs:1218-1239`). The trace records `error: invalid dimension 0 mm`, feature count three, and the same STEP digest as the 7.5 mm state. This rules out a refusal that partially mutated the feature or poisoned the next operation.
|
||||
|
||||
## Non-blocking hardening
|
||||
|
||||
Two small assertions would improve diagnosis without changing the coverage verdict:
|
||||
|
||||
1. Add `expect_feature_count: 2` and `expect_selection: none` before the face click. The later count of three and analytic 24,000 mm³ already catch an inert creation, but an explicit baseline would localize a changed starter fixture.
|
||||
2. Tighten the refusal substring from `dimension` to `invalid dimension 0 mm`. The current geometry equality and successful continuation make the broad match safe against the main false-green modes, while the exact text would better pin the intended command-boundary refusal.
|
||||
|
||||
Root's stated authoring corrections are consistent with the final evidence: the public selection spelling is `feature:pushpull`, and one timeline click selects that row while a second click would toggle it off. Neither correction indicates a production defect.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Existing push/pull parameter control — 2026-09-08
|
||||
|
||||
`functions-push-pull-edit.json` passes in release through actual face picks, the create card, timeline selection, typed parameter values and keyboard undo/redo. It supplements the existing push/pull creation/drag regression with a real-control test of `SetPushPullDistance`. No production fix was needed for this workflow.
|
||||
|
||||
The starter is 40 × 30 × 15 mm. Moving its top face by signed distance d gives volume `40 * 30 * (15 + d)`: creation at +5 mm produces 24000 mm³, editing the same feature to +7.5 produces 27000, and a subsequent inward edit to -2 produces 15600. Every export requires one six-face solid at relative tolerance 1e-9. Feature count stays at three after creation, so parameter editing cannot silently append a new operation.
|
||||
|
||||
Creation and the undo export have identical STEP bytes. The edited, redone and refused-zero-distance exports also have identical bytes. These equality observations were checked directly against the run's files; the committed script pins their geometry rather than byte digests. The zero edit must report `invalid dimension 0 mm`, leave the three-feature document and 27000 mm³ body intact, and allow the next typed negative value to succeed. STEP helpers only measure the result; all modeling uses actual controls.
|
||||
|
||||
Evidence from the release binary built at checkpoint `9c297b9`:
|
||||
|
||||
- `/tmp/vernier-push-pull-edit-pass/report.json`: pass, 63 steps, 85 frames, deterministic cross-process mode with distinct child IDs 14 and 73, llvmpipe; PNG checks skipped.
|
||||
- `/tmp/vernier-push-pull-edit-wrong.json` changes the first edited-body expectation to 27001. It exits 1 at step 27, reporting actual volume 27000. Evidence: `/tmp/vernier-push-pull-edit-wrong.log` and its report directory.
|
||||
- Two earlier script-authoring attempts corrected only the driver's exact `feature:pushpull` spelling and an unnecessary second timeline click that toggled the existing selection off. They are not production defects or acceptance passes.
|
||||
|
||||
The script is included in the regular modeling workflow test and `scripts/check.fish`. It will be rerun against the combined Trim changes before the next checkpoint. This covers one planar box face and signed distance editing; it does not certify arbitrary curved-face modeling or native hang containment.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Sketch tolerance follow-up — 2026-09-08
|
||||
|
||||
Read-only audit of checkpoint `9c297b9`, before the concurrent Trim correction. Source references below describe that checkpoint; the working tree began changing during the review. No production or test files were changed and no Cargo suite was run for this audit.
|
||||
|
||||
## Corrected scope of R4
|
||||
|
||||
The remaining fixed capture tolerance is **Trim endpoint capture**, not a shared drag/trim/split tolerance. Drawing already carries the six-physical-pixel camera conversion and Alt override. Drag has no position snapping to convert. Split already projects directly onto the chosen curve without the Trim snap.
|
||||
|
||||
| Route | Production rule at the checkpoint | Source |
|
||||
| --- | --- | --- |
|
||||
| Draw | `draw_snap_tolerance_mm` converts six physical pixels to a conservative local sketch-plane radius; Alt supplies `None` | `crates/vernier-app/src/camera.rs:16`; `app.rs:721` |
|
||||
| Start sketch drag | Movement exceeds 4 physical pixels; initial entity pick is within 10 physical pixels of a rendered segment | `camera.rs:275`; `drag.rs:182`; `crates/vernier-ui/src/canvas.rs:440` |
|
||||
| Drag position | Ray-plane intersection is sent unchanged to `Edit::DragSketchPoint`, then to the solver; no `snap_point` or Alt branch | `drag.rs:106`, `:133`, `:254`; `server/apply_draw.rs:587`; `crates/vernier-doc/src/command/sketch_geometry.rs:1125` |
|
||||
| Trim/Split curve pick | Nearest curve segment within the same 10-pixel radius; point-mark keys filtered out | `crates/vernier-app/src/cut.rs:40`, `:140` |
|
||||
| Trim endpoint capture | `trim_route` uses `TRIM_SNAP_MM = 2.0`; existing point gives Repoint or own-end refusal, otherwise ordinary trim | `server/apply_curve_edit.rs:254`; `server/mod.rs:953` |
|
||||
| Split position | `SplitSketchCurve` uses the projected split planner directly; no 2 mm capture | `server/apply_curve_edit.rs:185`; `crates/vernier-doc/src/command/plan.rs:1202` |
|
||||
|
||||
Trim reads only `SnapResult.point`. Axis alignment and grid calculations from the generic helper do not affect this route. Describing its defect as grid snapping would be inaccurate. The document's `POINT_COINCIDENCE_MM` is `1e-7` mm (`sketch.rs:1073`, `:2036`), a geometric validity threshold, not a UI capture radius.
|
||||
|
||||
There is a separate, source-proven target-size issue relevant to selection and starting a drag: point crosses are ±1 mm in model coordinates (`canvas.rs:30`, `:270`) and ordinary picking measures distance to their rendered arms (`:488`). Their on-screen extent therefore grows with zoom even though the added pick radius is ten pixels. A press can select a point far from its actual centre because it hits that cross. Trim/Split already exclude these marks. This does not establish a fixed drag-position snap, and adding snapping to dragging would be new interaction behavior requiring its own decision.
|
||||
|
||||
When a curve starts a drag, `drag.rs:220` chooses the nearest defining point in sketch-plane distance, using solved point rows. This can differ from screen-nearest on an oblique plane; it is another target-choice rule, not a capture tolerance. No separate failure reproduction was attempted here.
|
||||
|
||||
## Reproduction actually inspected
|
||||
|
||||
Root supplied two release real-control probes; both reports and the Trim script were read from disk:
|
||||
|
||||
- `/tmp/vernier-small-trim-red.json`: empty document, 1600 × 1000, camera distance 40, draw a 1 × 1 rectangle through the ribbon/tool and world clicks, arm Trim, click its bottom midpoint `(0.5, 0)`. `/tmp/vernier-small-trim-red/report.json` fails step 13, `expect_no_error`, with the own-end refusal. The two half-millimetre-away endpoints fall within the fixed 2 mm capture despite the useful screen separation.
|
||||
- `/tmp/vernier-small-split-red/report.json`: the corresponding Split probe passes all 14 steps, deterministic across distinct processes `[14, 73]`, llvmpipe. The directory suffix `red` is a historical probe name, not a failure verdict.
|
||||
|
||||
These probes establish the refusal distinction. They end at `expect_no_error` and have no export artifacts, so the Split pass alone is not proof of the resulting split topology or exported geometry.
|
||||
|
||||
## Requirements for the narrow correction
|
||||
|
||||
The proposed explicit `snap_tolerance_mm: Option<f64>` on `TrimCurve` and `CutHover` is coherent. Compute it through the existing camera conversion for both click and preview, with Alt disabling optional capture; leave Split's planner behavior unchanged. Preserve exact-own-end refusal separately by comparing the planner's projected `new_point` to both original endpoints with `POINT_COINCIDENCE_MM`. Checking only the raw pointer would miss an off-curve click which projects exactly onto an endpoint. Disabling capture must not mint a coincident but distinct replacement endpoint and silently open a loop.
|
||||
|
||||
Hover needs more than a tuple field. At the checkpoint, native `CursorMoved` submits it (`app.rs:1933`), while modifier changes only update input state (`:1804`). `Headless::set_cursor` refreshes solid ID hover but does not submit `CutHover` (`headless.rs:1072`). A shared frame refresh should recompute from current cursor, camera, modifiers and tool, preserve pointer-consumption/close/document-barrier gates, and retry after an in-flight job completes. Include tolerance in question identity, clear obsolete visible previews, and reject old answers. Test changes while the cursor remains still and while a worker response is pending.
|
||||
|
||||
A concrete existing reply-gate defect also needs attention: `app.rs:940` clears `scene.cut_preview` on `self.hover_at.is_none()`, which is the drawing-glyph question; the cut request is stored in `cut_hover_at`. Assert that an ordinary cut preview is present before testing its invalidation, so an always-absent preview cannot pass the negatives.
|
||||
|
||||
The planners and snap candidates currently read authored `SketchData`, whereas displayed geometry and point rows can use solved coordinates (`server/mod.rs:653`; `server/view.rs:228`). Projected references make the distinction meaningful. This is an existing consistency seam, not a demonstrated consequence of the radius conversion; do not claim the tolerance fix resolves it without a constrained/projected fixture.
|
||||
|
||||
## Adversarial acceptance fixture
|
||||
|
||||
Extend the one-millimetre probe at camera distances 40 and 80, verifying the relevant endpoint separation exceeds six physical pixels in each setup. After Trim, assert the fresh endpoint coordinate on the carrier, unchanged old shared endpoint identity, and open profile. Undo and redo through the actual controls must restore and reapply this change. Undo Trim, then Split the same side: assert five curves, the new shared split-point identity, closed profile, and an extrusion with analytic volume derived from the drawn dimensions. Export is a geometry oracle; it does not count as control coverage.
|
||||
|
||||
Add a nearby existing foreign point: within six pixels normal Trim must reuse it; Alt at the identical pixel must trim on the carrier without optional capture. Test an exact own-end projection with and without Alt and require the named refusal. Add positive preview assertions for each branch, then hold the cursor still while toggling Alt, changing zoom, changing Trim to Split, leaving sketch mode, and releasing a delayed worker response. A final click must agree with its preview.
|
||||
|
||||
Retain the existing point-cross exclusion control from `scripts/drive/m3-b-trim-split.json`. Its old refusal at 1.5 mm deliberately depends on the obsolete 2 mm radius, so any coordinate/expectation update must preserve a measured inside-radius refusal plus a separate cross-mark click reaching the curve. Its description of Split as avoiding snapping should be corrected: Split never used that snap route.
|
||||
|
||||
Audit frozen on 2026-09-08. This is a baseline finding and correction-requirements report, not acceptance of the concurrently edited implementation.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Trim screen tolerance review — 2026-09-08
|
||||
|
||||
**Disposition: no remaining blocker in the frozen Trim correction.** Independent read-only production/test review against checkpoint `9c297b9`, including the implementer's final foreign-point correction. Only this report was written. No broad Cargo gate was run by this reviewer.
|
||||
|
||||
## Correctness and closed findings
|
||||
|
||||
`crates/vernier-app/src/cut.rs:198` obtains the same conservative six-physical-pixel camera radius used by drawing, with Alt producing `None`. Both Trim click and preview carry that value. Split remains on its existing unsnapped projection path. The fixed `TRIM_SNAP_MM` constant is removed.
|
||||
|
||||
The projected-own-end guard is necessary when optional capture is disabled: a pointer off the line can still project exactly onto its original endpoint. The final guard uses `POINT_COINCIDENCE_MM` on the planner's projected point in the **no-point-snap branch** of `server/apply_curve_edit.rs:258`. Snapped own identities refuse; snapped foreign identities still repoint.
|
||||
|
||||
An introduced P2 was found and corrected during this review: the initial guard ran before snap selection. On the existing bottom line `(0,0) → (20,0)`, a request at the foreign point `(0,15)` with capture enabled incorrectly refused because its raw carrier projection was `(0,0)`. The final regression at `tests/m3_b_cut.rs:859` verifies both routes: capture enabled reuses the foreign identity with no new point, and capture disabled refuses the unchanged projected endpoint. Preview and actual command agree. `/tmp/vernier-trim-foreign-red.log` records the original failure; the independent final suite below includes its passing correction.
|
||||
|
||||
`cut.rs:85`, `:101`, `:184` now compute the complete current question from curve, position, tool and tolerance in the shared frame path. A changed question clears both request and visible answer before retrying. `Scene.cut_hover` (`scene.rs:74`) echoes the complete question from `server/mod.rs:698`; `app.rs:942` accepts a preview only for the matching outstanding question. The old drawing-hover gate is removed. The positive preview regression has genuine red evidence at `/tmp/vernier-trim-positive-preview-red.log`.
|
||||
|
||||
The frame path replaces the native-only cursor handler, so stationary Alt and camera changes also reach Headless. `pending_jobs` permits one outstanding hover; an unchanged answered question submits nothing. An intervening ordinary scene invalidates the cached question so the next idle frame refreshes against updated geometry. The existing close and document-barrier returns occur before dispatch. A successful background preview preserves the preceding user-edit refusal/readout (`app.rs:901`); an actual worker/compile error still replaces them and follows ordinary scene error handling. Hover remains a full compile/tessellation request, so this is bounded coalescing rather than process containment or a cheap preview codec.
|
||||
|
||||
The intermediate implementation had a second P2: pointer consumption was observed after the preview had already entered `FullOutput`, allowing a stationary popup to leave the last rendered refusal visible while native rendering slept. The final `app.rs:1342` reads pointer ownership after interactive UI and before `cut_overlay`; it suppresses both marks and text in that same frame. `/tmp/vernier-trim-popup-red.log` fails precisely the same-frame painted-text assertion. `/tmp/vernier-trim-popup-green.log` and the independent final run pass it. This test first proves the refusal was actually painted, opens the real Ctrl+K palette over the stationary curve cursor, inspects the returned painted output, then verifies preview recovery when the palette closes.
|
||||
|
||||
## Independently executed checks
|
||||
|
||||
With `CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target`, both commands exited 0 against the frozen final files:
|
||||
|
||||
- `cargo test -p vernier-app --lib tests::m3_b_cut:: -- --nocapture`: **20 passed**, no failures. Log: `/tmp/vernier-trim-review-cut.log`.
|
||||
- `cargo test -p vernier-app --test headless real_trim_hover_tracks_stationary_alt_and_camera_and_matches_the_click -- --exact --nocapture`: **1 passed**, exercising both camera distances 40 and 80. Log: `/tmp/vernier-trim-review-headless.log`.
|
||||
|
||||
The held-worker test (`tests/m3_b_cut.rs:680`) uses a real channel-gated worker. It proves one queued request, rejection of an old-radius reply after stationary Alt, one replacement request when idle, five unchanged frames with no new requests, pointer-panel invalidation, a held real Save barrier, and no new background work during close. Its corrected empty-document fixture avoids coincident curves from a starter sketch; the earlier ambiguous-fixture failure is not presented as a production defect.
|
||||
|
||||
The headless test (`tests/headless.rs:1539`) drives the actual tool, pointer and keyboard paths. It checks popup paint, Alt and zoom invalidation, exact preview-to-committed carrier position, unchanged original point/curve identities, open profile after Trim, and actual Ctrl+Z restoration. Native Save is used as a document oracle. After Undo, an isolated document-layer extrusion and STEP export/import validate 2 mm³, six faces and one solid; the captured trimmed topology fails compilation specifically because its profile is open. These helper calls are geometry evidence, not claimed Extrude/Export control coverage.
|
||||
|
||||
## Driven script evidence and limits
|
||||
|
||||
The small Trim scripts now assert the changed endpoint reference, exact original identities, bounded along-carrier coordinate, exact perpendicular coordinate, open profile, and actual Undo restoration. The legacy `m3-b-trim-split.json` retains Split topology assertions and strengthens the point-cross control by first proving an ordinary Select at the same aim picks `point:free`; Trim then ignores that mark and raises its own-end refusal after a successful Split has cleared the earlier error.
|
||||
|
||||
Inspected reports on disk:
|
||||
|
||||
- `/tmp/vernier-trim-small-40/report.json`: 34 steps, pass.
|
||||
- `/tmp/vernier-trim-small-80/report.json`: 34 steps, pass.
|
||||
- `/tmp/vernier-trim-legacy-final/report.json`: 40 steps, pass.
|
||||
|
||||
These three reports say **single process**, llvmpipe, and `deterministic: null`. They are not cross-process determinism evidence. Root owns final integrated/release gates; this review does not claim an unrun gate passed. The original 1 mm Trim refusal is recorded in `/tmp/vernier-small-trim-red/report.json`; its companion Split probe already passed before the correction.
|
||||
|
||||
Remaining scope is unchanged from `SKETCH_TOLERANCE_FOLLOWUP_2026-09-08.md`: ±1 mm point-mark extent still affects ordinary selection/drag starts; drag itself has no position snapping; authored-versus-solved sketch coordinates remain a separate unproven consistency seam. The present suite does not establish every oblique-plane, foreign-point GUI or redo interaction. Undo restoration and the exact foreign-point server route are verified as described above.
|
||||
|
||||
Report frozen on 2026-09-08. No production/test changes or commits by this reviewer.
|
||||
@@ -0,0 +1,70 @@
|
||||
# R4 Trim tolerance and shared cut hover — 2026-09-08
|
||||
|
||||
Implemented in the reliability worktree above checkpoint `9c297b9`; no commit, main integration, dependency, native-format, or naming-format change in this lane. Parent owns final combined/release gates and workflow-list registration. Production and tests are frozen. Independent review accepted the final lane with no remaining blocker; see `docs/TRIM_SCREEN_TOLERANCE_REVIEW_2026-09-08.md` (20 M3 cut tests and the actual headless test independently passed).
|
||||
|
||||
## Behavior and interface
|
||||
|
||||
`Edit::TrimCurve` and `Edit::CutHover` carry `snap_tolerance_mm: Option<f64>`. Actual cut click and preview use the drawing gesture's camera-derived local six-pixel tolerance, or `None` with Alt. Split still does not snap. The old fixed `TRIM_SNAP_MM = 2` is removed. Existing typed unit fixtures spell `Some(2.0)` explicitly when their purpose is the historical branch table.
|
||||
|
||||
A snapped point decides the target by identity: a curve's own endpoint refuses, a foreign point repoints without allocating another point. When no point is snapped, Trim checks the planned carrier point against both original endpoints using the existing `POINT_COINCIDENCE_MM = 1e-7`. This prevents a duplicate identity at an unchanged endpoint, including an off-carrier cursor that projects exactly onto an endpoint and Alt-disabled snapping. The geometric check does not preempt a valid foreign snap. Independent review caught that placement error in the initial implementation; the exact foreign-point red regression is retained in the evidence below.
|
||||
|
||||
`cut::CutQuestion` is `(EntityId, [f64; 2], SketchTool, Option<f64>)`. `Scene::cut_hover: Option<CutQuestion>` echoes the complete request. App reply handling accepts only the current question, independently of drawing `hover_at`. A successful background cut hover preserves a preceding edit refusal/readout; an actual hover compile error still replaces the error. A non-hover scene invalidates the old question because geometry may have changed. This is a typed worker field, not a serialized transport implementation.
|
||||
|
||||
The common frame loop refreshes cut hover for desktop and Headless. The native CursorMoved-only dispatcher is removed. A stationary Alt, camera, tool, or worker-idle transition can ask again; only one hover is outstanding, stale replies are dropped, and five unchanged frames are proven to submit no new job. Pointer ownership, active drag/orbit/pan, close, worker failure and document barriers remain gates.
|
||||
|
||||
Pointer ownership is snapshotted after interactive UI, before the noninteractive cut overlay, inside the egui closure. A popup over a stationary cursor suppresses the glyph in that same returned painted frame. State retirement and worker dispatch happen outside the closure. This avoids both egui re-entry and a stale glyph left in the native surface after state alone was cleared.
|
||||
|
||||
## Changed files
|
||||
|
||||
- `crates/vernier-app/src/{app,cut,edit,scene}.rs`
|
||||
- `crates/vernier-app/src/server/{mod,apply_curve_edit}.rs`
|
||||
- `crates/vernier-app/src/tests/m3_b_cut.rs` — branch, session identity, stale reply and bounded held-worker/frame tests.
|
||||
- `crates/vernier-app/src/tests/{m3_c,m3a_drag,preview}.rs` — mechanical `Scene::cut_hover: None` fixtures only.
|
||||
- `crates/vernier-app/tests/headless.rs` — appended actual-control Trim test at distances 40 and 80.
|
||||
- `scripts/drive/{small-trim-40,small-trim-80,m3-b-trim-split}.json`
|
||||
- This report.
|
||||
|
||||
No module was added; existing SOURCES entries cover the changed source files. Parent owns unrelated live changes, including registry, script lists, check.fish and other function-control scripts.
|
||||
|
||||
## Focused evidence
|
||||
|
||||
All Cargo commands use `CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target` and an outer timeout. UI tests use existing bounded helpers; the held-worker test also bounds each request/release wait. No competing workspace/release build was run by this lane.
|
||||
|
||||
| Instrument | Outcome | Evidence |
|
||||
|---|---|---|
|
||||
| Original real 1 mm Trim probe, distance 40 | Red at own-end refusal before correction | `/tmp/vernier-small-trim-red/report.json`, step 13 |
|
||||
| Positive real server preview with no drawing hover | Red on old app gate, then green | `/tmp/vernier-trim-positive-preview-red.log` |
|
||||
| Stationary real Ctrl+K palette covers a previously painted refusal | Red: returned frame still painted glyph after state cleared; then green with pre-overlay suppression | `/tmp/vernier-trim-popup-red.log`, `/tmp/vernier-trim-popup-green.log` |
|
||||
| Exact foreign point `(0,15)` for bottom edge `(0,0)-(20,0)` | Red under early projection guard; green with foreign snap precedence, exact point identity reuse and preview=commit; `None` still refuses | `/tmp/vernier-trim-foreign-red.log`, final cut log below |
|
||||
| `timeout 120 ... cargo test -p vernier-app --lib cut -- --nocapture` | 47 passed, including 20 M3 cut tests, routing/picking tests and impacted existing controls | `/tmp/vernier-trim-cut-final.log` |
|
||||
| `timeout 150 ... cargo test -p vernier-app --test headless real_trim_hover -- --nocapture` | 1 bounded test passed, internally runs both camera distances 40 and 80 | `/tmp/vernier-trim-headless-final.log` |
|
||||
| `timeout 180 ... cargo clippy -p vernier-app --all-targets -- -D warnings` | Passed | `/tmp/vernier-trim-clippy-final.log` |
|
||||
| Owned-file rustfmt check and `git diff --check` | Passed | Final lane check, exit 0 |
|
||||
|
||||
The actual Headless test creates the rectangle through the palette/tool menu and canvas, positively observes a preview and painted refusal, opens the real Ctrl+K palette over the held curve cursor (explicit `ModifiersChanged` event, matching native egui input), verifies the returned consumed frame contains no stale refusal, and verifies a fresh preview after closing it. Stationary Alt and camera changes clear/recompute the preview without PointerMoved. The actual Alt click creates exactly the displayed projected point and keeps every curve ID and original point. Actual Undo restores the original complete `SketchData`.
|
||||
|
||||
For the geometry oracle, the test loads that actual saved, undone document into an isolated `Document`, adds an explicitly named 2 mm extrusion, compiles, exports STEP, imports STEP independently and asserts volume 2 mm³ within 1e-9, six faces and one solid. The corresponding saved post-Trim open profile fails the same oracle compile specifically because its chain does not close. The helper extrusion proves restored topology/geometry; it is not presented as GUI extrusion coverage.
|
||||
|
||||
The own-end refusal test compares the full `Document::to_session_json()` bytes before and after every refused hover/commit across None, zero and positive radius, both endpoints, off-carrier projections, and an epsilon-close point. Thus current authored state, allocator ceiling and both history stacks remain unchanged. Dirty state also remains unchanged.
|
||||
|
||||
## Driven scripts and intentional changes
|
||||
|
||||
The parent-built intermediate release binary passed single runs (`--once --skip-png`) of:
|
||||
|
||||
- `small-trim-40.json`: 34 steps, `/tmp/vernier-trim-small-40/report.json`.
|
||||
- `small-trim-80.json`: 34 steps, `/tmp/vernier-trim-small-80/report.json`.
|
||||
- Revised `m3-b-trim-split.json`: 40 steps, `/tmp/vernier-trim-legacy-final/report.json`.
|
||||
|
||||
These are intermediate single-run script evidence, not a claim of final rebuilt or cross-process release verification. Root will rebuild the frozen code and run final workflows. Both small scripts require exactly one new point (ID 10), unchanged original points and curve IDs, an exact carrier coordinate `y=0`, an allocator ceiling of 11, and the complete original points/curves after actual Undo. Their midpoint x coordinates measured 0.4885555200853666 at distance 40 and 0.46728814725828016 at distance 80; the 0.06 mm bound covers integer-pixel quantization while remaining far from either endpoint.
|
||||
|
||||
The existing cut script deliberately replaces the old 2 mm-specific refusal aim `(50,1.5)` with `(50,0.25)`, inside the local six-pixel radius. Its inside-point-cross aim becomes `(49.85,0.15)`. A newly added actual Select click first requires `point:free` at this exact aim, then actual Trim at the same aim requires the own-end refusal. This proves mark exclusion instead of assuming it. The successful Split before the second refusal requires `expect_no_error`, so the previous error cannot satisfy that refusal. Original Trim/Split document coordinates and identity assertions remain.
|
||||
|
||||
The initial rectangle click becomes exactly `(50,0)`. The earlier oblique aim `(49.5,0.3)` actually produced `(49,0)` after pixel quantization with current drawing tolerance, despite old notes claiming `(50,0)`. Saved native fixture evidence is `/tmp/vernier-trim-legacy-probe/run1/probe.vernier`. A refusal aimed at x=50 would otherwise address the wrong location. The exact grid aim makes the intended 20 x 15 rectangle explicit.
|
||||
|
||||
## Limits and remaining work
|
||||
|
||||
- Independent focused review is complete. Rebuilt release/cross-process workflows and the combined workspace gate belong to root after this freeze; root has registered the two small scripts in the native-topology driver gate and check script.
|
||||
- Undo intentionally leaves active sketch/chain state. Re-selecting an existing line profile after Undo/Open cannot currently re-arm the extrusion card: extend_chain is reached by drawing and the picked-circle fallback does not generalize to line chains. This is an existing unsupported workflow (R7), not a Trim fix or a claimed GUI extrusion pass.
|
||||
- Planners/snapping use authored sketch points while displayed sketch geometry may use solved points. This separate seam was not reproduced or changed here.
|
||||
- Drag has no snap; its pick/drag thresholds and the model-space point-cross extent are separate behaviors. This lane does not claim to fix them.
|
||||
- The existing worker remains thread-based. Hover coalescing and bounded tests do not constitute native process hang containment; the R13 worker transport stage remains separate.
|
||||
@@ -188,6 +188,12 @@ and cargo run -q -p vernier-drive -- scripts/drive/small-profile-40.json \
|
||||
--out target/drive/small-profile-40 --require-adapter RADV
|
||||
and cargo run -q -p vernier-drive -- scripts/drive/small-profile-80.json \
|
||||
--out target/drive/small-profile-80 --require-adapter RADV
|
||||
# These two assert trimmed and undone native sketch topology; the headless
|
||||
# companion test supplies the restored-profile STEP geometry oracle.
|
||||
and cargo run -q -p vernier-drive -- scripts/drive/small-trim-40.json \
|
||||
--out target/drive/small-trim-40 --require-adapter RADV
|
||||
and cargo run -q -p vernier-drive -- scripts/drive/small-trim-80.json \
|
||||
--out target/drive/small-trim-80 --require-adapter RADV
|
||||
and cargo run -q -p vernier-drive -- scripts/drive/snap-alt-override.json \
|
||||
--out target/drive/snap-alt-override --require-adapter RADV
|
||||
and cargo run -q -p vernier-drive -- scripts/drive/cut-then-boss.json \
|
||||
@@ -220,3 +226,9 @@ and cargo run -q -p vernier-drive -- scripts/drive/save-preserves-selection.json
|
||||
--out target/drive/save-preserves-selection --require-adapter RADV
|
||||
and cargo run -q -p vernier-drive -- scripts/drive/save-preserves-sketch.json \
|
||||
--out target/drive/save-preserves-sketch --require-adapter RADV
|
||||
and cargo run -q -p vernier-drive -- scripts/drive/functions-hole.json \
|
||||
--out target/drive/functions-hole --require-adapter RADV
|
||||
and cargo run -q -p vernier-drive -- scripts/drive/functions-push-pull-edit.json \
|
||||
--out target/drive/functions-push-pull-edit --require-adapter RADV
|
||||
and cargo run -q -p vernier-drive -- scripts/drive/functions-pattern-count.json \
|
||||
--out target/drive/functions-pattern-count --require-adapter RADV
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"name": "plain-hole-real-controls",
|
||||
"notes": [
|
||||
"Actual MakeHole and EditHole controls. The STEP export command is used only as the measured geometry oracle.",
|
||||
"The starter is a 40 x 30 x 15 mm box. Two real, locked sketch points on its top face are drilled together.",
|
||||
"Creation presses the painted Hole control at its 6 mm Through All defaults. Editing types diameter 10 mm and depth 6 mm and turns Through off through the selected hole's real value card.",
|
||||
"Through volume = 18000 - 2*pi*(6/2)^2*15. Blind volume = 18000 - 2*pi*(10/2)^2*6.",
|
||||
"Ctrl+Z and Ctrl+Y are real keyboard shortcut events sent through the shell for creation and parameter editing. Counterbore is outside this plain-hole gate because no counterbore controls are painted."
|
||||
],
|
||||
"size": [1600, 1000],
|
||||
"camera": { "target": [20.0, 15.0, 7.5], "distance": 120.0 },
|
||||
"steps": [
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 2 },
|
||||
{ "step": "expect_selection", "is": "none" },
|
||||
|
||||
{ "step": "click", "at": { "by": "face", "face": { "by": "named", "feature": "block",
|
||||
"key": { "by": "role", "role": "prism-end" } } } },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_selection", "is": "faces:1:planar" },
|
||||
{ "step": "click", "at": "ribbon:sketch" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 3 },
|
||||
|
||||
{ "step": "click", "at": "text:Point" },
|
||||
{ "step": "click", "at": "world:12,15,15" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "click", "at": "world:28,15,15" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
|
||||
{ "step": "click", "at": "text:Select" },
|
||||
{ "step": "click", "at": "world:12,15,15" },
|
||||
{ "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:28,15,15" },
|
||||
{ "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:12,15,15" },
|
||||
{ "step": "click", "at": "world:28,15,15", "ctrl": true },
|
||||
{ "step": "frames", "count": 2 },
|
||||
{ "step": "expect_selection", "is": "two-points" },
|
||||
{ "step": "click", "at": "ribbon:hole" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 4 },
|
||||
{ "step": "expect_timeline", "labels": ["block outline — sketch", "block — extrude", "face sketch — sketch", "hole — hole"] },
|
||||
{ "step": "export_step", "path": "{out}/hole-through-d6.step" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_step", "path": "{out}/hole-through-d6.step", "volume": 17151.769983530758,
|
||||
"solids": 1, "tol": 1e-9 },
|
||||
|
||||
{ "step": "key", "key": "ctrl+z" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 3 },
|
||||
{ "step": "expect_timeline", "labels": ["block outline — sketch", "block — extrude", "face sketch — sketch"] },
|
||||
{ "step": "key", "key": "ctrl+y" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 4 },
|
||||
{ "step": "expect_timeline", "labels": ["block outline — sketch", "block — extrude", "face sketch — sketch", "hole — hole"] },
|
||||
{ "step": "export_step", "path": "{out}/hole-through-d6-redone.step" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_step", "path": "{out}/hole-through-d6-redone.step", "volume": 17151.769983530758,
|
||||
"solids": 1, "tol": 1e-9 },
|
||||
|
||||
{ "step": "click", "at": "timeline:3" },
|
||||
{ "step": "frames", "count": 2 },
|
||||
{ "step": "expect_selection", "is": "feature:hole" },
|
||||
{ "step": "click", "at": "chip:through" },
|
||||
{ "step": "frames", "count": 1 },
|
||||
{ "step": "click", "at": "card:diameter" },
|
||||
{ "step": "key", "key": "ctrl+a" },
|
||||
{ "step": "type", "text": "10" },
|
||||
{ "step": "click", "at": "card:depth" },
|
||||
{ "step": "key", "key": "ctrl+a" },
|
||||
{ "step": "type", "text": "6" },
|
||||
{ "step": "key", "key": "Enter" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 4 },
|
||||
{ "step": "export_step", "path": "{out}/hole-blind-d10-depth6.step" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_step", "path": "{out}/hole-blind-d10-depth6.step", "volume": 17057.52220392306,
|
||||
"solids": 1, "tol": 1e-9 },
|
||||
|
||||
{ "step": "key", "key": "ctrl+z" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 4 },
|
||||
{ "step": "export_step", "path": "{out}/hole-edit-undone.step" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_step", "path": "{out}/hole-edit-undone.step", "volume": 17151.769983530758,
|
||||
"solids": 1, "tol": 1e-9 },
|
||||
{ "step": "key", "key": "ctrl+y" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_feature_count", "is": 4 },
|
||||
{ "step": "export_step", "path": "{out}/hole-edit-redone.step" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_step", "path": "{out}/hole-edit-redone.step", "volume": 17057.52220392306,
|
||||
"solids": 1, "tol": 1e-9 },
|
||||
{ "step": "expect_no_warnings" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
{
|
||||
"name": "pattern-count-actual-card",
|
||||
"size": [
|
||||
1600,
|
||||
1000
|
||||
],
|
||||
"camera": {
|
||||
"target": [
|
||||
20,
|
||||
15,
|
||||
7.5
|
||||
],
|
||||
"distance": 120
|
||||
},
|
||||
"notes": [
|
||||
"Actual linear pattern creation uses count3 and dx10 on the40x30x15 starter extrude. Overlap makes one60x30x15 box.",
|
||||
"The selected pattern count card must change that same feature to5, yielding80x30x15; CtrlZ/Y, refused count1 and subsequent count2 must preserve the expected geometry.",
|
||||
"STEP is an independent geometry oracle. Pattern spacing editing is not claimed by this count control."
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "timeline:1"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "ribbon:pattern"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:count"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+a"
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "3"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:dx"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+a"
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "10"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 3
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/pattern-created.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/pattern-created.step",
|
||||
"volume": 27000,
|
||||
"solids": 1,
|
||||
"faces": 6,
|
||||
"tol": 1e-09
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "timeline:2"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "feature:pattern"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:count"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+a"
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "5"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 3
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/pattern-edited.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/pattern-edited.step",
|
||||
"volume": 36000,
|
||||
"solids": 1,
|
||||
"faces": 6,
|
||||
"tol": 1e-09
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+z"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 3
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/pattern-undone.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/pattern-undone.step",
|
||||
"volume": 27000,
|
||||
"faces": 6,
|
||||
"solids": 1,
|
||||
"tol": 1e-09
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+y"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 3
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/pattern-redone.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/pattern-redone.step",
|
||||
"volume": 36000,
|
||||
"faces": 6,
|
||||
"solids": 1,
|
||||
"tol": 1e-09
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:count"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+a"
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "1"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_error",
|
||||
"contains": "pattern count 1 outside 2..=64"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 3
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/pattern-refused.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/pattern-refused.step",
|
||||
"volume": 36000,
|
||||
"faces": 6,
|
||||
"solids": 1,
|
||||
"tol": 1e-09
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:count"
|
||||
},
|
||||
{
|
||||
"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": 3
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/pattern-shrunk.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/pattern-shrunk.step",
|
||||
"volume": 22500,
|
||||
"faces": 6,
|
||||
"solids": 1,
|
||||
"tol": 1e-09
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
{
|
||||
"name": "push-pull-parameter-edit-and-refusal",
|
||||
"size": [
|
||||
1600,
|
||||
1000
|
||||
],
|
||||
"camera": {
|
||||
"target": [
|
||||
20,
|
||||
15,
|
||||
7.5
|
||||
],
|
||||
"distance": 120
|
||||
},
|
||||
"notes": [
|
||||
"Actual face card creates a 5 mm push/pull; the timeline distance card edits that same feature to 7.5 mm and -2 mm.",
|
||||
"Undo/redo and a refused zero-distance edit must preserve the corresponding analytic 40 x 30 x (15 + distance) mm body. STEP export is only the geometry oracle."
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "none"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": {
|
||||
"by": "face",
|
||||
"face": {
|
||||
"by": "named",
|
||||
"feature": "block",
|
||||
"key": {
|
||||
"by": "role",
|
||||
"role": "prism-end"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "faces:1:planar"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:distance"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+a"
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "5"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 3
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/created.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/created.step",
|
||||
"volume": 24000,
|
||||
"faces": 6,
|
||||
"solids": 1,
|
||||
"tol": 1e-09
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "timeline:2"
|
||||
},
|
||||
{
|
||||
"step": "frames",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "feature:pushpull"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:distance"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+a"
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "7.5"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 3
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/edited.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/edited.step",
|
||||
"volume": 27000,
|
||||
"faces": 6,
|
||||
"solids": 1,
|
||||
"tol": 1e-09
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+z"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 3
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/undone.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/undone.step",
|
||||
"volume": 24000,
|
||||
"faces": 6,
|
||||
"solids": 1,
|
||||
"tol": 1e-09
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+y"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 3
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/redone.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/redone.step",
|
||||
"volume": 27000,
|
||||
"faces": 6,
|
||||
"solids": 1,
|
||||
"tol": 1e-09
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "feature:pushpull"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:distance"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+a"
|
||||
},
|
||||
{
|
||||
"step": "type",
|
||||
"text": "0"
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_error",
|
||||
"contains": "invalid dimension 0 mm"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 3
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/after-refusal.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/after-refusal.step",
|
||||
"volume": 27000,
|
||||
"faces": 6,
|
||||
"solids": 1,
|
||||
"tol": 1e-09
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "card:distance"
|
||||
},
|
||||
{
|
||||
"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": 3
|
||||
},
|
||||
{
|
||||
"step": "export_step",
|
||||
"path": "{out}/inward.step"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_step",
|
||||
"path": "{out}/inward.step",
|
||||
"volume": 15600,
|
||||
"faces": 6,
|
||||
"solids": 1,
|
||||
"tol": 1e-09
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,94 +1,13 @@
|
||||
{
|
||||
"name": "m3-b-trim-split",
|
||||
"notes": [
|
||||
"M3 LANE B, THE CUT TOOLS, THROUGH THE REAL SHELL: draw a rectangle beside",
|
||||
"the starter block, trim one side, watch the profile OPEN, click a corner",
|
||||
"and read the refusal, then split another side. Every number is derived",
|
||||
"here, because JSON has no comments.",
|
||||
"",
|
||||
"The sketch is on world XY with nothing picked, and the rectangle is drawn",
|
||||
"at (50, 0) - (70, 15) -- clear of the starter block, which occupies",
|
||||
"x 0..40, y 0..30 -- so no click can be ambiguous between the two sketches.",
|
||||
"",
|
||||
"STEP: TRIM. The bottom side runs (50,0) -> (70,0). A click at u = 62 is",
|
||||
"past its midpoint (len = 20, mid = 10, t = 12), so the FAR corner (70,0)",
|
||||
"is the one that moves and lands at (62,0). Nothing is within the 2 mm",
|
||||
"draw-snap of that click -- the corners are 12 and 8 mm away -- so this is",
|
||||
"the branch table's NO-SNAP row: a fresh point on the curve's own carrier.",
|
||||
"The right side still starts at the old corner, so the loop is OPEN, and",
|
||||
"`expect_selection none` is what says so: a closed profile would report",
|
||||
"`profile`, and that is the assertion.",
|
||||
"",
|
||||
"STEP: THE REFUSAL. A click at (50, 1.5) is ON the left side (x = 50) and",
|
||||
"1.5 mm from the corner (50,0), inside the 2 mm snap -- so it snaps onto",
|
||||
"one of that curve's OWN endpoints. That is the row the M3 spec's first",
|
||||
"draft got wrong: `plan_trim_curve_endpoint` does NOT refuse it (it reads",
|
||||
"t = 0, moving_len = len), so it would mint a duplicate point at the same",
|
||||
"coordinates and silently orphan the shared corner. The arm refuses BY",
|
||||
"NAME instead, and `expect_error` reads the words.",
|
||||
"",
|
||||
"It is 1.5 mm and not 0.5 for a reason that USED TO BE a hole: a point's",
|
||||
"cross mark is +/-1 mm of model space (canvas::POINT_MARK_MM) and is",
|
||||
"pickable in its own right, so a click inside the cross resolved to the",
|
||||
"POINT and the cut was dropped in silence -- not refused. 1.5 mm is",
|
||||
"outside the cross and inside the 2 mm snap, so this step tested the",
|
||||
"refusal without going near the hole.",
|
||||
"",
|
||||
"STEP: INSIDE THE CROSS, which is the click that hole swallowed. The",
|
||||
"aim is (49.5, 0.3): 0.3 mm from the corner mark\u0027s horizontal arm, 0.5",
|
||||
"from the left side and 0.58 from the bottom one, so the NEAREST drawn",
|
||||
"segment is the point mark and an ordinary selection there resolves to",
|
||||
"the POINT -- measured, not assumed: with the marks put back into the cut",
|
||||
"pick this step goes green-to-red and the click is dropped in silence.",
|
||||
"Dead on the corner would not test it, because there the curve and the",
|
||||
"mark are both at distance 0 and document order hands the tie to the",
|
||||
"curve anyway. `cut::curve_at` leaves",
|
||||
"the marks out of a CUT pick, so the click reaches the branch table and",
|
||||
"refuses BY NAME instead of doing nothing. Which of the two curves it",
|
||||
"lands on does not matter: the corner is an endpoint of both, so both",
|
||||
"refuse in the same words. It is asserted AFTER the split so the readout",
|
||||
"it reads cannot be the earlier refusal still standing -- the split",
|
||||
"clears the error, and re-arming Trim is a shell-only click that raises",
|
||||
"no scene of its own.",
|
||||
"",
|
||||
"STEP: SPLIT. A click at (60, 15) is the midpoint region of the top side",
|
||||
"((70,15) -> (50,15)), 10 mm from either corner, so it splits rather than",
|
||||
"snapping: one new point, one new curve.",
|
||||
"",
|
||||
"THE DOCUMENT POINTERS ARE THE GEOMETRY ASSERTION HERE (see the tolerance",
|
||||
"note below for which half of each point is exact). The starter document mints 1-8 (the block",
|
||||
"outline's points and curves), 9 (its sketch), 10 (the extrude), and 11-16",
|
||||
"(the compiled body's six face ids, from the same allocator). So:",
|
||||
" 17 = this sketch",
|
||||
" 18 = the rectangle's first corner (the first click places a point)",
|
||||
" 19, 20, 21 = the other three corners (the second click)",
|
||||
" 22, 23, 24, 25 = its four sides",
|
||||
" 26 = the point the TRIM minted, at (62, 0)",
|
||||
" 27 = the point the SPLIT minted, at (60, 15)",
|
||||
"If the id allocation ever moves, these assertions are SUPPOSED to break",
|
||||
"rather than quietly read some other point.",
|
||||
"",
|
||||
"THE TOLERANCES SAY WHICH HALF OF EACH POINT IS EXACT, and that split is",
|
||||
"the assertion rather than a concession. A click is a PIXEL: the world",
|
||||
"point it resolves to is wherever the ray through that pixel's centre",
|
||||
"meets the sketch plane, so the coordinate ALONG the curve is quantized by",
|
||||
"the pixel grid -- measured here at 61.9247 for a click aimed at 62, i.e.",
|
||||
"0.075 mm, about one pixel at this camera. 0.2 mm is roughly three.",
|
||||
"The coordinate ACROSS the curve is exact to 1e-12, and must be: both",
|
||||
"planners project onto the curve's own carrier, so a trimmed end that",
|
||||
"landed at y != 0, or a split point off the top side, would mean the point",
|
||||
"was placed at the click instead of on the curve.",
|
||||
"",
|
||||
"THE WINDOW IS THE DEFAULT 1600, AND IT USED TO BE 2400. The reason it",
|
||||
"gave -- m3-b-project.json's measurement that at 1600 the sketch ribbon's",
|
||||
"tail is clipped -- was real and is now gone: the ribbon measures its row",
|
||||
"and collapses it from the right, so `text:Trim` and `text:Split` are",
|
||||
"reached through the tool row's own `more` chevron when they do not fit.",
|
||||
"This script is one of the things that says so at the size a person runs.",
|
||||
"",
|
||||
"NO SCREENSHOT: the preview marks are painted by an egui overlay whose",
|
||||
"golden would be a second m1-smoke, and what this script is about is what",
|
||||
"the CLICKS do."
|
||||
"Trim and Split through actual collapsed tool controls at 1600 x 1000. The starter block is separate from a 20 x 15 rectangle at (50,0)-(70,15).",
|
||||
"Trim at (62,0) moves the nearer endpoint of the bottom side and opens the profile. Split at (60,15) adds one shared endpoint and one curve. Native document assertions check the along-carrier coordinate within pixel quantization and the across-carrier coordinate at 1e-12.",
|
||||
"R4 deliberately changes the old 2 mm capture controls to the camera-derived six-pixel radius. At this camera the previous refusal aim (50,1.5) is outside that radius and is now a valid trim. The replacement (50,0.25) is inside the local screen-space capture radius and still requires the same own-end refusal.",
|
||||
"The second refusal uses (49.85,0.15), inside the point cross and the new capture radius. An actual Select click first asserts point:free at that same aim, proving point-mark capture; Cut then excludes that mark and reaches the curve endpoint refusal. It follows a successful Split, so a stale prior error cannot satisfy the assertion.",
|
||||
"The initial click is exactly (50,0). The prior oblique aim (49.5,0.3) actually drew (49,0) after pixel quantization, contradicting its old fixture notes and making a refusal at x=50 target the wrong location. This exact grid aim makes the intended rectangle explicit.",
|
||||
"Native IDs are intentional: starter sketch1-8, sketch9, extrusion10, body11, faces12-17; new sketch18, corner19 and further corners20-22, curves23-26, Trim point27, Split point28. A shifted allocator sequence must break these assertions.",
|
||||
"Trim and Split both project their new point onto the authored curve carrier. This gate does not claim to fix the separate authored-versus-solved coordinate seam or drag behavior."
|
||||
],
|
||||
"size": [1600, 1000],
|
||||
"camera": { "target": [40.0, 10.0, 0.0], "distance": 160.0 },
|
||||
@@ -103,7 +22,7 @@
|
||||
{ "step": "expect_feature_count", "is": 3 },
|
||||
|
||||
{ "step": "click", "at": "text:Rectangle" },
|
||||
{ "step": "click", "at": "world:49.5,0.3,0" },
|
||||
{ "step": "click", "at": "world:50,0,0" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "click", "at": "world:70,15,0" },
|
||||
{ "step": "wait_idle" },
|
||||
@@ -116,7 +35,7 @@
|
||||
{ "step": "expect_no_error" },
|
||||
{ "step": "expect_selection", "is": "none" },
|
||||
|
||||
{ "step": "click", "at": "world:50,1.5,0" },
|
||||
{ "step": "click", "at": "world:50,0.25,0" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_error", "contains": "own end" },
|
||||
|
||||
@@ -125,8 +44,11 @@
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_no_error" },
|
||||
|
||||
{ "step": "click", "at": "text:Select" },
|
||||
{ "step": "click", "at": "world:49.85,0.15,0" },
|
||||
{ "step": "expect_selection", "is": "point:free" },
|
||||
{ "step": "click", "at": "text:Trim" },
|
||||
{ "step": "click", "at": "world:49.5,0.3,0" },
|
||||
{ "step": "click", "at": "world:49.85,0.15,0" },
|
||||
{ "step": "wait_idle" },
|
||||
{ "step": "expect_error", "contains": "own end" },
|
||||
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"name": "one-millimetre-trim-at-distance-40",
|
||||
"document": "empty",
|
||||
"size": [
|
||||
1600,
|
||||
1000
|
||||
],
|
||||
"camera": {
|
||||
"target": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"distance": 40
|
||||
},
|
||||
"notes": [
|
||||
"Actual Rectangle, Trim and Undo controls on a 1 x 1 mm profile. A midpoint trim must succeed at both camera distances instead of capturing an endpoint 0.5 mm away with the old fixed 2 mm radius.",
|
||||
"Native Save steps are document oracles. The trim adds exactly one endpoint (ID10), changes only the chosen endpoint reference, retains all four original points/curve identities, and leaves the carrier coordinate exactly y=0. The x tolerance0.06mm bounds pixel quantization at these cameras; it does not allow endpoint capture.",
|
||||
"Undo restores the complete original points/curves while retaining allocator ceiling11. The bounded actual-headless companion test verifies preview=commit, stationary Alt/zoom/popup behavior, and the restored sketch with an independent STEP geometry oracle.",
|
||||
"Drawing auto-constraints remain the original four orthogonal rectangle constraints. This script does not claim drag snapping or repair of the authored-versus-solved coordinate seam."
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "ribbon:sketch"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:Rectangle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:0,0,0"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:1,1,0"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "profile"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:Trim"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:0.5,0,0"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 1
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "none"
|
||||
},
|
||||
{
|
||||
"step": "save",
|
||||
"path": "{out}/trim.vernier"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/ids/next",
|
||||
"equals": 11
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points/2",
|
||||
"equals": [
|
||||
0.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points/3",
|
||||
"equals": [
|
||||
1.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points/4",
|
||||
"equals": [
|
||||
1.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points/5",
|
||||
"equals": [
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/curves",
|
||||
"equals": {
|
||||
"6": {
|
||||
"Line": {
|
||||
"start": 10,
|
||||
"end": 3
|
||||
}
|
||||
},
|
||||
"7": {
|
||||
"Line": {
|
||||
"start": 3,
|
||||
"end": 4
|
||||
}
|
||||
},
|
||||
"8": {
|
||||
"Line": {
|
||||
"start": 4,
|
||||
"end": 5
|
||||
}
|
||||
},
|
||||
"9": {
|
||||
"Line": {
|
||||
"start": 5,
|
||||
"end": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points/10/0",
|
||||
"equals": 0.5,
|
||||
"tol": 0.06
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points/10/1",
|
||||
"equals": 0,
|
||||
"tol": 1e-12
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+z"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "save",
|
||||
"path": "{out}/undo.vernier"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/undo.vernier",
|
||||
"pointer": "/ids/next",
|
||||
"equals": 11
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/undo.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points",
|
||||
"equals": {
|
||||
"2": [
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"3": [
|
||||
1.0,
|
||||
0.0
|
||||
],
|
||||
"4": [
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"5": [
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/undo.vernier",
|
||||
"pointer": "/state/features/1/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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"name": "one-millimetre-trim-at-distance-80",
|
||||
"document": "empty",
|
||||
"size": [
|
||||
1600,
|
||||
1000
|
||||
],
|
||||
"camera": {
|
||||
"target": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"distance": 80
|
||||
},
|
||||
"notes": [
|
||||
"Actual Rectangle, Trim and Undo controls on a 1 x 1 mm profile. A midpoint trim must succeed at both camera distances instead of capturing an endpoint 0.5 mm away with the old fixed 2 mm radius.",
|
||||
"Native Save steps are document oracles. The trim adds exactly one endpoint (ID10), changes only the chosen endpoint reference, retains all four original points/curve identities, and leaves the carrier coordinate exactly y=0. The x tolerance0.06mm bounds pixel quantization at these cameras; it does not allow endpoint capture.",
|
||||
"Undo restores the complete original points/curves while retaining allocator ceiling11. The bounded actual-headless companion test verifies preview=commit, stationary Alt/zoom/popup behavior, and the restored sketch with an independent STEP geometry oracle.",
|
||||
"Drawing auto-constraints remain the original four orthogonal rectangle constraints. This script does not claim drag snapping or repair of the authored-versus-solved coordinate seam."
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "ribbon:sketch"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:Rectangle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:0,0,0"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:1,1,0"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "profile"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "text:Trim"
|
||||
},
|
||||
{
|
||||
"step": "click",
|
||||
"at": "world:0.5,0,0"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "expect_feature_count",
|
||||
"is": 1
|
||||
},
|
||||
{
|
||||
"step": "expect_selection",
|
||||
"is": "none"
|
||||
},
|
||||
{
|
||||
"step": "save",
|
||||
"path": "{out}/trim.vernier"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/ids/next",
|
||||
"equals": 11
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points/2",
|
||||
"equals": [
|
||||
0.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points/3",
|
||||
"equals": [
|
||||
1.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points/4",
|
||||
"equals": [
|
||||
1.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points/5",
|
||||
"equals": [
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/curves",
|
||||
"equals": {
|
||||
"6": {
|
||||
"Line": {
|
||||
"start": 10,
|
||||
"end": 3
|
||||
}
|
||||
},
|
||||
"7": {
|
||||
"Line": {
|
||||
"start": 3,
|
||||
"end": 4
|
||||
}
|
||||
},
|
||||
"8": {
|
||||
"Line": {
|
||||
"start": 4,
|
||||
"end": 5
|
||||
}
|
||||
},
|
||||
"9": {
|
||||
"Line": {
|
||||
"start": 5,
|
||||
"end": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points/10/0",
|
||||
"equals": 0.5,
|
||||
"tol": 0.06
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/trim.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points/10/1",
|
||||
"equals": 0,
|
||||
"tol": 1e-12
|
||||
},
|
||||
{
|
||||
"step": "key",
|
||||
"key": "ctrl+z"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_no_error"
|
||||
},
|
||||
{
|
||||
"step": "save",
|
||||
"path": "{out}/undo.vernier"
|
||||
},
|
||||
{
|
||||
"step": "wait_idle"
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/undo.vernier",
|
||||
"pointer": "/ids/next",
|
||||
"equals": 11
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/undo.vernier",
|
||||
"pointer": "/state/features/1/payload/Sketch/points",
|
||||
"equals": {
|
||||
"2": [
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"3": [
|
||||
1.0,
|
||||
0.0
|
||||
],
|
||||
"4": [
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"5": [
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"step": "expect_document",
|
||||
"path": "{out}/undo.vernier",
|
||||
"pointer": "/state/features/1/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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user