mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 20:02:16 +00:00
Engine: one sketch store for the sketch commands (W38.0, #5425, no behaviour change)
The sketch commands found and kept the sketch they work on in about forty places, each its own way. They now go through one module, cmd::sketch_store, so a drawing session (W38a/b) can put sheet and view sketches behind the same calls without the commands changing again. - target: the command's `sketch` param or the sketch being edited (target_sketch, moved). - active: the sketch being edited, if any. The thirteen commands that need one keep their own refusal text; unifying the three texts is a separate, visible change. - read and write: the only calls from the sketch commands to Document::sketch and sketch_mut. - update: change a sketch on a copy of the document and keep both, the edit the text and link commands made by hand (sketch.text, sketch.edit_text, the projection, intersection and include links, sketch.fit_curves_to_section, sketch.break_link). As before, those paths neither solve the sketch nor run its checks. - checked: the checks an edited sketch passes (the periodic splines), shared by edit_solved and sketch.offset.break. edit and edit_solved stay in sketch.rs and load and keep their sketch through read and write. Imported text, snap points and the face projections keep their own shape (a text that can't be linked is skipped, snap points copy the document only when one is needed) through read and write. The reads and writes that are about the design's own sketches stay as they are, each on a line marked `// design-only:` (Redefine Sketch Plane, a joint's curve, a motion trace's wire, and the session's live drag, parameter edits and `created` diff). A source scan, each_sketch_command_goes_through_the_store, fails when a command reads or writes a sketch, or asks for the active one, anywhere but the store or a marked design-only line in the files that work on the design's sketches. Every existing sketch test is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -929,6 +929,7 @@ fn redefine_sketch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
&& super::sketch::face::anchor(&plane)
|
||||
.is_none_or(|next| next.target != anchor.target || next.component != anchor.component || next.path != anchor.path)
|
||||
{
|
||||
// design-only: Redefine Sketch Plane edits the design's sketch feature.
|
||||
let sketch = trial.doc_mut().sketch_mut(id)?;
|
||||
if let Some(mut visible) = visible_geometry {
|
||||
let constraints: std::collections::BTreeMap<_, _> =
|
||||
|
||||
@@ -184,6 +184,7 @@ fn face_ref(doc: &Document, name: &str) -> Value {
|
||||
// A side swept from a sketch curve (a later profile's too, `side:s<k>.<curve>`).
|
||||
if let Base::Made { role: Role::Side { curve, .. }, .. } = &face.base {
|
||||
let found = f.kind.input_sketches().into_iter().find_map(|id| {
|
||||
// design-only: a joint names a curve of one of the design's sketches.
|
||||
let sk = doc.sketch(id).ok()?;
|
||||
let c = sk.curves.iter().find(|c| c.id == *curve)?;
|
||||
let curve_type = serde_json::to_value(&c.kind).ok().and_then(|k| k.get("type").cloned()).unwrap_or(Value::Null);
|
||||
|
||||
@@ -87,6 +87,7 @@ mod sketch_import;
|
||||
mod sketch_inspect;
|
||||
mod sketch_modify;
|
||||
mod sketch_project;
|
||||
mod sketch_store;
|
||||
mod surface;
|
||||
mod surface_analysis;
|
||||
mod surface_boundary;
|
||||
|
||||
@@ -254,6 +254,7 @@ fn seeded(s: &Session, t: &MotionTrace) -> Session {
|
||||
pub(super) fn remember(s: &mut Session, t: &MotionTrace) {
|
||||
let key = inputs(s, t).map_err(|e| e.to_string());
|
||||
if let Ok(key) = &key
|
||||
// design-only: a motion trace's wire is in one of the design's sketches.
|
||||
&& let Some(points) = s.doc.sketch(t.sketch).ok().and_then(|sk| sk.wires.iter().find(|w| w.id == t.wire)).map(|w| w.fit.clone())
|
||||
{
|
||||
remember_points(s, t.sketch, key.clone(), points);
|
||||
@@ -330,6 +331,7 @@ pub(crate) fn refresh(s: &mut Session) {
|
||||
let unchanged = s.motion_traces.entries.get(&t.sketch).is_some_and(|old| old.inputs == key);
|
||||
if unchanged
|
||||
&& cached.as_ref().is_none_or(|points| {
|
||||
// design-only: a motion trace's wire is in one of the design's sketches.
|
||||
s.doc.sketch(t.sketch).ok().and_then(|sk| sk.wires.iter().find(|w| w.id == t.wire)).is_some_and(|w| same_points(&w.fit, points))
|
||||
})
|
||||
{
|
||||
@@ -352,9 +354,11 @@ pub(crate) fn refresh(s: &mut Session) {
|
||||
remember_points(s, t.sketch, key.clone(), points.clone());
|
||||
}
|
||||
let error = match result {
|
||||
// design-only: a motion trace's wire is in one of the design's sketches.
|
||||
Ok(points) => match s.doc.sketch(t.sketch).ok().and_then(|sk| sk.wires.iter().find(|w| w.id == t.wire)) {
|
||||
Some(wire) if same_points(&wire.fit, &points) => None,
|
||||
Some(_) => {
|
||||
// design-only: a motion trace's wire is in one of the design's sketches.
|
||||
if let Ok(sk) = s.doc_mut().sketch_mut(t.sketch)
|
||||
&& let Some(wire) = sk.wires.iter_mut().find(|w| w.id == t.wire)
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ mod dimensioned;
|
||||
mod drag;
|
||||
#[path = "sketch_face.rs"]
|
||||
pub(super) mod face;
|
||||
use super::sketch_store;
|
||||
use dimensioned::draw as draw_dimensioned;
|
||||
use drag::drag_entity;
|
||||
|
||||
@@ -212,23 +213,6 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
|
||||
/// Resolve the sketch a command acts on: `sketch` param (id or name) or the active sketch.
|
||||
pub(super) fn target_sketch(s: &Session, p: &Value, cmd: &str) -> Result<u64> {
|
||||
if let Some(v) = p.get("sketch") {
|
||||
let key = match v {
|
||||
Value::Number(n) => n.to_string(),
|
||||
Value::String(x) => x.clone(),
|
||||
_ => return Err(bad(cmd, "`sketch` must be an id or a name")),
|
||||
};
|
||||
let f = s.doc.find_feature(&key).ok_or_else(|| bad(cmd, format!("no sketch `{key}`")))?;
|
||||
if !matches!(f.kind, FeatureKind::Sketch { .. }) {
|
||||
return Err(bad(cmd, format!("`{key}` is not a sketch")));
|
||||
}
|
||||
return Ok(f.id);
|
||||
}
|
||||
s.active_sketch.ok_or_else(|| EngineError::Other("no sketch is being edited".into()))
|
||||
}
|
||||
|
||||
/// Edit a sketch: clone it, apply `f`, solve with the document's dimension values and store
|
||||
/// it. With `strict`, an edit that makes the constraints unsolvable is rejected.
|
||||
pub(super) fn edit<T>(
|
||||
@@ -238,7 +222,7 @@ pub(super) fn edit<T>(
|
||||
strict: bool,
|
||||
f: impl FnOnce(&mut Sketch, &mut solidcraft_doc::Document) -> Result<T>,
|
||||
) -> Result<(T, Value)> {
|
||||
let id = target_sketch(s, p, cmd)?;
|
||||
let id = sketch_store::target(s, p, cmd)?;
|
||||
let (out, rep) = edit_solved(s, id, strict, Analysis::Freedom, |sk, doc| Ok((f(sk, doc)?, Left::Unsolved)))?;
|
||||
Ok((out, rep.map(|rep| report_json(&rep)).unwrap_or(Value::Null)))
|
||||
}
|
||||
@@ -282,7 +266,7 @@ pub(super) fn edit_solved<T>(
|
||||
f: impl FnOnce(&mut Sketch, &mut solidcraft_doc::Document) -> Result<(T, Left)>,
|
||||
) -> Result<(T, Option<SolveReport>)> {
|
||||
let mut doc = (*s.doc).clone();
|
||||
let mut sk = doc.sketch(id)?.clone();
|
||||
let mut sk = sketch_store::read(&doc, id)?.clone();
|
||||
if sk.patterns.iter().any(|p| p.count_expression.is_some() || !p.expressions.is_empty()) {
|
||||
doc.apply_pattern_values(&doc.param_values().0, &mut sk)?;
|
||||
}
|
||||
@@ -329,8 +313,8 @@ pub(super) fn edit_solved<T>(
|
||||
Some(settle(strict, rep, &mut sk, unsolved)?)
|
||||
}
|
||||
};
|
||||
sk.validate_periodic_splines()?;
|
||||
*doc.sketch_mut(id)? = sk;
|
||||
sketch_store::checked(&sk)?;
|
||||
sketch_store::write(&mut doc, id, sk)?;
|
||||
*s.doc_mut() = doc;
|
||||
Ok((out, rep))
|
||||
}
|
||||
@@ -862,7 +846,7 @@ fn entry_point(s: &Session) -> SketchEntry {
|
||||
/// and Undo is the way back.
|
||||
fn cancel_sketch(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.cancel";
|
||||
let id = s.active_sketch.ok_or_else(|| bad(cmd, "no sketch is being edited"))?;
|
||||
let id = sketch_store::active(s).ok_or_else(|| bad(cmd, "no sketch is being edited"))?;
|
||||
let entry = s.sketch_entry.clone().filter(|entry| entry.sketch == id);
|
||||
let current = entry.filter(|entry| {
|
||||
let top = entry.undo_len.checked_sub(1).and_then(|i| s.undo.get(i)).map(|snap| &snap.doc);
|
||||
@@ -1507,7 +1491,12 @@ fn draw_point(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// end refuses it, rather than answered as if a point had been made.
|
||||
let made = str_(p, "point")
|
||||
.filter(|_| p.get("point") != Some(&given))
|
||||
.filter(|id| target_sketch(s, p, cmd).ok().and_then(|sid| s.doc.sketch(sid).ok()).is_some_and(|sk| sk.resolve_point(id).is_some()))
|
||||
.filter(|id| {
|
||||
sketch_store::target(s, p, cmd)
|
||||
.ok()
|
||||
.and_then(|sid| sketch_store::read(&s.doc, sid).ok())
|
||||
.is_some_and(|sk| sk.resolve_point(id).is_some())
|
||||
})
|
||||
.map(str::to_string);
|
||||
let Some(pid) = made else {
|
||||
return Err(bad(
|
||||
@@ -1729,7 +1718,7 @@ pub(super) fn redundancy(
|
||||
/// The constraints that already determine constraint `cid` of the target sketch, as
|
||||
/// "id (Name)".
|
||||
fn determined_by(s: &Session, p: &Value, cmd: &str, cid: &str) -> Vec<String> {
|
||||
let Some(sk) = target_sketch(s, p, cmd).ok().and_then(|id| s.doc.sketch(id).ok()) else { return Vec::new() };
|
||||
let Some(sk) = sketch_store::target(s, p, cmd).ok().and_then(|id| sketch_store::read(&s.doc, id).ok()) else { return Vec::new() };
|
||||
let Some(ci) = sk.constraints.iter().position(|c| c.id == cid) else { return Vec::new() };
|
||||
solidcraft_sketch::determined_by(sk, ci)
|
||||
.into_iter()
|
||||
@@ -1863,7 +1852,7 @@ fn c_horizontal_vertical(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn c_coincident(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.constraint.coincident";
|
||||
// Already tied (e.g. connected when it was drawn there): nothing to add.
|
||||
let sk = s.doc.sketch(target_sketch(s, p, cmd)?)?;
|
||||
let sk = sketch_store::read(&s.doc, sketch_store::target(s, p, cmd)?)?;
|
||||
if let Some((a, b)) = two_round(sk, p) {
|
||||
return coincident_circles(s, p, cmd, a, b);
|
||||
}
|
||||
@@ -2649,7 +2638,7 @@ fn measured(is_angle: bool, v: f64) -> (f64, &'static str) {
|
||||
|
||||
/// What the dimension just added (the sketch's last constraint) measures on the solved sketch.
|
||||
fn solved_measure(s: &Session, p: &Value, cmd: &str) -> Option<(f64, &'static str)> {
|
||||
let sk = s.doc.sketch(target_sketch(s, p, cmd).ok()?).ok()?;
|
||||
let sk = sketch_store::read(&s.doc, sketch_store::target(s, p, cmd).ok()?).ok()?;
|
||||
let k = &sk.constraints.last()?.kind;
|
||||
Some(measured(k.is_angle(), solidcraft_sketch::measure_dimension(sk, k)?))
|
||||
}
|
||||
@@ -2920,7 +2909,7 @@ fn sketch_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn move_point(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.move_point";
|
||||
let to = req_vec2(cmd, p, "to")?;
|
||||
let id = target_sketch(s, p, cmd)?;
|
||||
let id = sketch_store::target(s, p, cmd)?;
|
||||
let state = s.model.result(id).map(|r| r.state.clone());
|
||||
let last = state.as_ref().and_then(|state| state.sketch(id));
|
||||
let (_, rep) = edit_solved(s, id, false, Analysis::Freedom, |sk, _| {
|
||||
@@ -2946,7 +2935,7 @@ fn drag_frame(s: &mut Session, p: &Value, live: bool) -> Result<(Value, Option<c
|
||||
None | Some(Value::Null) => to,
|
||||
Some(v) => vec2(v).ok_or_else(|| bad(cmd, "`from` must be a point [x, y]"))?,
|
||||
};
|
||||
let id = target_sketch(s, p, cmd)?;
|
||||
let id = sketch_store::target(s, p, cmd)?;
|
||||
// The sketch's report as last evaluated: a live frame keeps its degrees of freedom.
|
||||
let state = s.model.result(id).map(|r| r.state.clone());
|
||||
let last = state.as_ref().and_then(|state| state.sketch(id));
|
||||
@@ -2983,7 +2972,7 @@ fn drag_frame(s: &mut Session, p: &Value, live: bool) -> Result<(Value, Option<c
|
||||
})?;
|
||||
// Lines retain the grabbed material point through rotation. Other entities report the
|
||||
// average displacement of their moving points, as before.
|
||||
let sk = s.doc.sketch(id)?;
|
||||
let sk = sketch_store::read(&s.doc, id)?;
|
||||
let at = d.at(sk, from);
|
||||
let rep = rep.or_else(|| last.map(|ss| SolveReport { status: solidcraft_sketch::SolveStatus::Solved, failing: Vec::new(), ..ss.report.clone() }));
|
||||
let out = json!({"sketch": rep.as_ref().map(report_json), "reached": d.reached, "at": [at.x, at.y], "retry": d.retry});
|
||||
@@ -2995,7 +2984,7 @@ fn drag_frame(s: &mut Session, p: &Value, live: bool) -> Result<(Value, Option<c
|
||||
/// evaluate first.
|
||||
fn drag_end(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.drag_end";
|
||||
let id = target_sketch(s, p, cmd)?;
|
||||
let id = sketch_store::target(s, p, cmd)?;
|
||||
let start = s.doc.clone();
|
||||
let mut constraint = Value::Null;
|
||||
if let Some(c) = p.get("constraint").filter(|c| !c.is_null()) {
|
||||
@@ -3058,8 +3047,8 @@ fn sketch_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let set = |k: &str| bool_(p, k);
|
||||
if keys.iter().all(|k| set(k).is_none()) {
|
||||
let id = target_sketch(s, p, cmd)?;
|
||||
return Ok(json!({"options": options_json(&s.doc.sketch(id)?.view)}));
|
||||
let id = sketch_store::target(s, p, cmd)?;
|
||||
return Ok(json!({"options": options_json(&sketch_store::read(&s.doc, id)?.view)}));
|
||||
}
|
||||
let (v, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let v = &mut sk.view;
|
||||
@@ -3085,14 +3074,14 @@ fn sketch_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn chain_cmd(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.chain";
|
||||
let id = target_sketch(s, p, cmd)?;
|
||||
let sk = s.doc.sketch(id)?;
|
||||
let id = sketch_store::target(s, p, cmd)?;
|
||||
let sk = sketch_store::read(&s.doc, id)?;
|
||||
let c = curve_ref(sk, p.get("curve"), cmd, "curve")?;
|
||||
Ok(json!({"curves": ids_of(sk, &sk.chain(c))}))
|
||||
}
|
||||
|
||||
fn solve_cmd(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = target_sketch(s, p, "sketch.solve")?;
|
||||
let id = sketch_store::target(s, p, "sketch.solve")?;
|
||||
let st = s.model.state();
|
||||
let ss = st.sketch(id).ok_or_else(|| EngineError::Other("the sketch has not been evaluated".into()))?;
|
||||
Ok(json!({"status": ss.report.status, "dof": ss.report.dof, "failing": ss.report.failing, "profiles": ss.profiles.len()}))
|
||||
|
||||
@@ -6,7 +6,8 @@ use serde_json::{Value, json};
|
||||
use solidcraft_geom::Vec3;
|
||||
use solidcraft_sketch::WireSpace;
|
||||
|
||||
use super::sketch::{edit, target_sketch};
|
||||
use super::sketch::edit;
|
||||
use super::sketch_store;
|
||||
use super::{CommandSpec, in_sketch};
|
||||
use crate::params::{bad, req_vec3, vec3};
|
||||
use crate::{Result, Session};
|
||||
@@ -45,7 +46,7 @@ fn needs_3d(sk: &solidcraft_sketch::Sketch, cmd: &str) -> Result<()> {
|
||||
/// World command inputs belong to the target sketch's owner, which may differ from the
|
||||
/// active component when a script supplies an explicit sketch.
|
||||
fn inverse_owner_frame(s: &Session, p: &Value, cmd: &str) -> Result<solidcraft_doc::Mat> {
|
||||
let id = target_sketch(s, p, cmd)?;
|
||||
let id = sketch_store::target(s, p, cmd)?;
|
||||
let owner = s.doc.feature(id).map(|f| f.component).ok_or_else(|| bad(cmd, "target sketch is missing"))?;
|
||||
if owner != 0 && owner != s.active_component {
|
||||
if s.doc.occurrences.len() > 100_000 {
|
||||
|
||||
@@ -11,7 +11,8 @@ use serde_json::{Value, json};
|
||||
use solidcraft_geom::{Aabb2, Vec2};
|
||||
use solidcraft_sketch::{ConstraintKind, CurveKind, LinkKind, LinkSource, Sketch};
|
||||
|
||||
use super::sketch::{edit, target_sketch};
|
||||
use super::sketch::edit;
|
||||
use super::sketch_store;
|
||||
use super::{CommandSpec, in_sketch};
|
||||
use crate::params::{bad, string_list, vec2};
|
||||
use crate::{Result, Sel, Session};
|
||||
@@ -91,12 +92,12 @@ fn text_of(sk: &Sketch, ci: usize) -> Option<&str> {
|
||||
|
||||
fn copy(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.copy";
|
||||
let id = target_sketch(s, p, cmd)?;
|
||||
let id = sketch_store::target(s, p, cmd)?;
|
||||
let ids = chosen(s, p);
|
||||
if ids.is_empty() {
|
||||
return Err(bad(cmd, "select the sketch curves or points to copy"));
|
||||
}
|
||||
let sk = s.doc.sketch(id)?;
|
||||
let sk = sketch_store::read(&s.doc, id)?;
|
||||
let (mut cs, mut ps, mut texts): (Vec<usize>, Vec<usize>, Vec<String>) = (Vec::new(), Vec::new(), Vec::new());
|
||||
for e in &ids {
|
||||
if let Some(c) = sk.curve_index(e) {
|
||||
@@ -244,7 +245,7 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(cmd, "params must be an object"));
|
||||
}
|
||||
let Some(clip) = s.sketch_clipboard.clone() else { return Err(bad(cmd, "nothing copied (sketch.copy first)")) };
|
||||
let target = target_sketch(s, p, cmd)?;
|
||||
let target = sketch_store::target(s, p, cmd)?;
|
||||
let positioned = p.get("at").is_some() || p.get("translate").is_some();
|
||||
let t = if let Some(v) = p.get("at") {
|
||||
vec2(v).ok_or_else(|| bad(cmd, "`at` must be [x, y]"))? - clip.center
|
||||
|
||||
@@ -6,6 +6,7 @@ use solidcraft_geom::Vec2;
|
||||
use solidcraft_sketch::{ConstraintKind, CurveKind, Sketch};
|
||||
|
||||
use super::sketch::{add_c, constraint_result, dof_now, edit, listed, redundancy, residual_of};
|
||||
use super::sketch_store;
|
||||
use super::{CommandSpec, in_sketch};
|
||||
use crate::params::{bad, num, req_vec2, str_, string_list, vec2};
|
||||
use crate::{EngineError, Result, Session};
|
||||
@@ -396,8 +397,8 @@ fn toggle_driven(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})?;
|
||||
if !driven {
|
||||
// Making it driving must not over-constrain: compare with the sketch without it.
|
||||
let id = s.active_sketch.ok_or_else(|| bad(cmd, "no sketch"))?;
|
||||
let mut sk = s.doc.sketch(id)?.clone();
|
||||
let id = sketch_store::active(s).ok_or_else(|| bad(cmd, "no sketch"))?;
|
||||
let mut sk = sketch_store::read(&s.doc, id)?.clone();
|
||||
if let Some(c) = sk.constraints.iter_mut().find(|c| c.id == cid) {
|
||||
c.driven = true;
|
||||
}
|
||||
@@ -498,7 +499,7 @@ fn snap(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let at = req_vec2(cmd, p, "at")?;
|
||||
let from = p.get("from").and_then(vec2);
|
||||
let r = num(p, "radius").filter(|r| *r > 0.0).unwrap_or(1.0);
|
||||
let id = s.active_sketch.ok_or_else(|| bad(cmd, "no sketch is being edited"))?;
|
||||
let id = sketch_store::active(s).ok_or_else(|| bad(cmd, "no sketch is being edited"))?;
|
||||
let st = s.model.state();
|
||||
let ss = st.sketch(id).ok_or_else(|| EngineError::Other("the sketch is not evaluated".into()))?;
|
||||
let sk = &ss.sketch;
|
||||
|
||||
@@ -13,6 +13,7 @@ macro_rules! text_style_doc {
|
||||
}
|
||||
|
||||
use super::sketch::{add_c, circumcircle, edit, ids_of, mark_construction, req_parg, result, slot};
|
||||
use super::sketch_store;
|
||||
use super::{CommandSpec, in_sketch};
|
||||
use crate::params::{bad, num, req_vec2, str_, string_list};
|
||||
use crate::{Result, Session};
|
||||
@@ -272,20 +273,19 @@ fn text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(cmd, AT_MUST));
|
||||
}
|
||||
let src = text_source(&s.doc, p, cmd, None)?;
|
||||
let id = s.active_sketch.ok_or_else(|| bad(cmd, "no sketch is being edited"))?;
|
||||
let mut doc = (*s.doc).clone();
|
||||
let mut sk = doc.sketch(id)?.clone();
|
||||
// A text needs nothing outside its sketch (a path is one of its curves).
|
||||
let geom = sk.text_source_geometry(&src).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
if geom.is_empty() {
|
||||
return Err(bad(cmd, "the text makes no outline"));
|
||||
}
|
||||
let guide = guide(&mut sk, &src);
|
||||
let id = sketch_store::active(s).ok_or_else(|| bad(cmd, "no sketch is being edited"))?;
|
||||
let exprs = text_exprs_json(&src);
|
||||
let l = sk.add_link(LinkKind::Text, src, &geom)?;
|
||||
let curves = ids_of(&sk, &sk.link_curves(&l));
|
||||
*doc.sketch_mut(id)? = sk;
|
||||
*s.doc_mut() = doc;
|
||||
let (l, curves, guide) = sketch_store::update(s, id, |_, _, sk| {
|
||||
// A text needs nothing outside its sketch (a path is one of its curves).
|
||||
let geom = sk.text_source_geometry(&src).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
if geom.is_empty() {
|
||||
return Err(bad(cmd, "the text makes no outline"));
|
||||
}
|
||||
let guide = guide(sk, &src);
|
||||
let l = sk.add_link(LinkKind::Text, src, &geom)?;
|
||||
let curves = ids_of(sk, &sk.link_curves(&l));
|
||||
Ok((l, curves, guide))
|
||||
})?;
|
||||
let mut out = json!({"link": l, "curves": curves, "guide": guide});
|
||||
if let Some(exprs) = exprs {
|
||||
out["exprs"] = exprs;
|
||||
@@ -360,23 +360,20 @@ const MAX_TEXT_EDITS: usize = 1000;
|
||||
|
||||
fn edit_text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.edit_text";
|
||||
let id = s.active_sketch.ok_or_else(|| bad(cmd, "no sketch is being edited"))?;
|
||||
let mut doc = (*s.doc).clone();
|
||||
let mut sk = doc.sketch(id)?.clone();
|
||||
// The numbers its expressions give now (a parameter may have changed since the sketch was
|
||||
// last stored); one that cannot be evaluated is left as it was, to be replaced by this edit.
|
||||
let _ = doc.apply_text_values(&doc.param_values().0, &mut sk);
|
||||
let out = apply_text_edits(&mut sk, &doc, p)?;
|
||||
*doc.sketch_mut(id)? = sk;
|
||||
*s.doc_mut() = doc;
|
||||
Ok(out)
|
||||
let id = sketch_store::active(s).ok_or_else(|| bad(cmd, "no sketch is being edited"))?;
|
||||
sketch_store::update(s, id, |_, doc, sk| {
|
||||
// The numbers its expressions give now (a parameter may have changed since the sketch was
|
||||
// last stored); one that cannot be evaluated is left as it was, to be replaced by this edit.
|
||||
let _ = doc.apply_text_values(&doc.param_values().0, sk);
|
||||
apply_text_edits(sk, doc, p)
|
||||
})
|
||||
}
|
||||
|
||||
/// One frame of an interactive resize or rotation: the sketch is solved and shown, the
|
||||
/// features after it keep their last result until `sketch.drag_end` evaluates the model.
|
||||
fn edit_text_live(s: &mut Session, p: &Value) -> Result<(Value, Option<crate::LiveEdit>)> {
|
||||
let cmd = "sketch.edit_text";
|
||||
let id = s.active_sketch.ok_or_else(|| bad(cmd, "no sketch is being edited"))?;
|
||||
let id = sketch_store::active(s).ok_or_else(|| bad(cmd, "no sketch is being edited"))?;
|
||||
let (out, report) = super::sketch::edit_solved(s, id, false, solidcraft_sketch::Analysis::Full, |sk, doc| {
|
||||
Ok((apply_text_edits(sk, doc, p)?, super::sketch::Left::Unsolved))
|
||||
})?;
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_sketch::{ConstraintKind, EndReport, EndTouch, Sketch, loose_ends};
|
||||
|
||||
use super::sketch::{add_c, edit, target_sketch};
|
||||
use super::sketch::{add_c, edit};
|
||||
use super::sketch_store;
|
||||
use super::{CommandSpec, in_sketch};
|
||||
use crate::params::{bad, string_list};
|
||||
use crate::{EngineError, Result, Session};
|
||||
@@ -53,7 +54,7 @@ fn report_json(sk: &Sketch, r: &EndReport) -> Value {
|
||||
}
|
||||
|
||||
fn loose_ends_cmd(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = target_sketch(s, p, "sketch.loose_ends")?;
|
||||
let id = sketch_store::target(s, p, "sketch.loose_ends")?;
|
||||
let st = s.model.state();
|
||||
let ss = st.sketch(id).ok_or_else(|| EngineError::Other("the sketch has not been evaluated".into()))?;
|
||||
Ok(report_json(&ss.sketch, &ss.ends))
|
||||
|
||||
@@ -9,6 +9,7 @@ use solidcraft_sketch::{CurveKind, LinkKind, LinkSource, Sketch};
|
||||
|
||||
use super::CommandSpec;
|
||||
use super::sketch::{edit, ids_of};
|
||||
use super::sketch_store;
|
||||
use crate::params::{bad, num, str_, vec2};
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
@@ -141,12 +142,13 @@ fn insert(s: &mut Session, p: &Value, cmd: &str, svg: bool) -> Result<Value> {
|
||||
Ok((ids_of(sk, &cs), texts))
|
||||
})?;
|
||||
// Text becomes sketch text.
|
||||
let id = s.active_sketch.ok_or_else(|| bad(cmd, "no sketch"))?;
|
||||
let id = sketch_store::active(s).ok_or_else(|| bad(cmd, "no sketch"))?;
|
||||
let mut n_text = 0;
|
||||
for t in texts {
|
||||
if let Geom2::Text { text, at, height, angle } = t {
|
||||
// Not `sketch_store::update`: a text that can't be linked is skipped, not refused.
|
||||
let mut doc = (*s.doc).clone();
|
||||
let mut sk = doc.sketch(id)?.clone();
|
||||
let mut sk = sketch_store::read(&doc, id)?.clone();
|
||||
if super::sketch_project::add_link(
|
||||
s,
|
||||
&doc,
|
||||
@@ -157,7 +159,7 @@ fn insert(s: &mut Session, p: &Value, cmd: &str, svg: bool) -> Result<Value> {
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
*doc.sketch_mut(id)? = sk;
|
||||
sketch_store::write(&mut doc, id, sk)?;
|
||||
*s.doc_mut() = doc;
|
||||
n_text += 1;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use solidcraft_geom::Vec2;
|
||||
use solidcraft_sketch::{ConstraintKind, Curve, CurveKind, Shape, Sketch, intersections, intersections_within};
|
||||
|
||||
use super::sketch::{add_c, edit, ids_of, solve_current};
|
||||
use super::sketch_store;
|
||||
use super::{CommandSpec, in_sketch};
|
||||
use crate::params::{bad, bool_, expr, num, req_expr, req_vec2, str_, string_list, vec2, vec2_list};
|
||||
use crate::{Result, Session};
|
||||
@@ -2227,7 +2228,7 @@ fn offset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn break_offset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.offset.break";
|
||||
let sketch = super::sketch::target_sketch(s, p, cmd)?;
|
||||
let sketch = sketch_store::target(s, p, cmd)?;
|
||||
let (out, info) = edit(s, p, cmd, true, |sk, doc| {
|
||||
let output = str_(p, "curve").ok_or_else(|| bad(cmd, "`curve` must name an offset output"))?;
|
||||
let record = sk.freeform_offsets.iter().position(|record| record.output == output);
|
||||
@@ -2247,7 +2248,7 @@ fn break_offset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !rep.keeps_orientation() {
|
||||
return Err(bad(cmd, rep.failure(|_| "cannot break an offset with conflicting dimensions".into())));
|
||||
}
|
||||
sk.validate_periodic_splines()?;
|
||||
sketch_store::checked(sk)?;
|
||||
if let Some(association) = sk
|
||||
.offsets
|
||||
.iter()
|
||||
|
||||
@@ -9,6 +9,7 @@ use solidcraft_doc::{Document, FeatureKind};
|
||||
use solidcraft_geom::{Plane, Vec2, Vec3};
|
||||
use solidcraft_sketch::{ConstraintKind, LinkKind, LinkSource, Sketch, nearest_crossing};
|
||||
|
||||
use super::sketch_store;
|
||||
use super::{CommandSpec, in_sketch};
|
||||
use crate::params::{bad, bool_, str_, vec3};
|
||||
use crate::{EngineError, Result, Session};
|
||||
@@ -198,7 +199,7 @@ pub(super) fn refresh(s: &Session, doc: &Document, id: u64, sk: &mut Sketch) ->
|
||||
Some(anchor) => solidcraft_doc::project::refresh_face_anchor_links(&doc, &vals, &st, &plane, sk, anchor),
|
||||
None => solidcraft_doc::project::refresh_links(&doc, &vals, &st, &plane, sk),
|
||||
};
|
||||
if let Ok(stored) = doc.sketch(id)
|
||||
if let Ok(stored) = sketch_store::read(&doc, id)
|
||||
&& sk.positions_in(stored).is_some()
|
||||
{
|
||||
let mut solved = sk.clone();
|
||||
@@ -292,28 +293,28 @@ fn run_links(s: &mut Session, p: &Value, cmd: &str, kind: LinkKind) -> Result<Va
|
||||
if refs.is_empty() {
|
||||
return Err(bad(cmd, "`refs` is empty"));
|
||||
}
|
||||
let id = s.active_sketch.ok_or_else(|| EngineError::Other("no sketch is being edited".into()))?;
|
||||
let id = sketch_store::active(s).ok_or_else(|| EngineError::Other("no sketch is being edited".into()))?;
|
||||
let sources: Vec<LinkSource> = refs.iter().map(|v| source(s, v, cmd)).collect::<Result<_>>()?;
|
||||
let mut doc = (*s.doc).clone();
|
||||
let mut sk = doc.sketch(id)?.clone();
|
||||
refresh(s, &doc, id, &mut sk);
|
||||
let mut out = Vec::new();
|
||||
// Projecting what the sketch already shows (the border of the face it is on, an edge
|
||||
// projected before) would lay a second copy over the first: those references are skipped.
|
||||
let mut present = Vec::new();
|
||||
for src in sources {
|
||||
let what = src.describe();
|
||||
match add_link_unless_present(s, &doc, id, &mut sk, kind, src, kind == LinkKind::Project) {
|
||||
Ok(Some(l)) => out.push(link_json(&sk, &l)),
|
||||
Ok(None) => present.push(what),
|
||||
Err(e) => return Err(bad(cmd, format!("{what}: {e}"))),
|
||||
let (out, present) = sketch_store::update(s, id, |s, doc, sk| {
|
||||
refresh(s, doc, id, sk);
|
||||
let mut out = Vec::new();
|
||||
// Projecting what the sketch already shows (the border of the face it is on, an edge
|
||||
// projected before) would lay a second copy over the first: those references are skipped.
|
||||
let mut present = Vec::new();
|
||||
for src in sources {
|
||||
let what = src.describe();
|
||||
match add_link_unless_present(s, doc, id, sk, kind, src, kind == LinkKind::Project) {
|
||||
Ok(Some(l)) => out.push(link_json(sk, &l)),
|
||||
Ok(None) => present.push(what),
|
||||
Err(e) => return Err(bad(cmd, format!("{what}: {e}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
if out.is_empty() {
|
||||
return Err(bad(cmd, format!("already projected into this sketch: {}", present.join(", "))));
|
||||
}
|
||||
*doc.sketch_mut(id)? = sk;
|
||||
*s.doc_mut() = doc;
|
||||
// Nothing new: the sketch is kept as it was.
|
||||
if out.is_empty() {
|
||||
return Err(bad(cmd, format!("already projected into this sketch: {}", present.join(", "))));
|
||||
}
|
||||
Ok((out, present))
|
||||
})?;
|
||||
let mut r = json!({"links": out});
|
||||
if !present.is_empty() {
|
||||
r["already_projected"] = json!(present);
|
||||
@@ -335,18 +336,16 @@ fn include_cmd(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// Add one link from `src` to the active sketch and report it.
|
||||
fn one_link(s: &mut Session, cmd: &str, kind: LinkKind, src: LinkSource) -> Result<Value> {
|
||||
let id = s.active_sketch.ok_or_else(|| EngineError::Other("no sketch is being edited".into()))?;
|
||||
let mut doc = (*s.doc).clone();
|
||||
let mut sk = doc.sketch(id)?.clone();
|
||||
refresh(s, &doc, id, &mut sk);
|
||||
let what = src.describe();
|
||||
let l = add_link(s, &doc, id, &mut sk, kind, src).map_err(|e| bad(cmd, format!("{what}: {e}")))?;
|
||||
let mut out = link_json(&sk, &l);
|
||||
let wires: Vec<String> = sk.link_wires(&l).iter().filter_map(|w| sk.wires.get(*w).map(|w| w.id.clone())).collect();
|
||||
out["wires"] = json!(wires);
|
||||
*doc.sketch_mut(id)? = sk;
|
||||
*s.doc_mut() = doc;
|
||||
Ok(out)
|
||||
let id = sketch_store::active(s).ok_or_else(|| EngineError::Other("no sketch is being edited".into()))?;
|
||||
sketch_store::update(s, id, |s, doc, sk| {
|
||||
refresh(s, doc, id, sk);
|
||||
let what = src.describe();
|
||||
let l = add_link(s, doc, id, sk, kind, src).map_err(|e| bad(cmd, format!("{what}: {e}")))?;
|
||||
let mut out = link_json(sk, &l);
|
||||
let wires: Vec<String> = sk.link_wires(&l).iter().filter_map(|w| sk.wires.get(*w).map(|w| w.id.clone())).collect();
|
||||
out["wires"] = json!(wires);
|
||||
Ok(out)
|
||||
})
|
||||
}
|
||||
|
||||
fn project_to_surface(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -389,7 +388,7 @@ fn spun_profile(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"Y" => (Vec3::ZERO, Vec3::Y),
|
||||
"Z" => (Vec3::ZERO, Vec3::Z),
|
||||
_ => {
|
||||
let id = s.active_sketch.ok_or_else(|| EngineError::Other("no sketch is being edited".into()))?;
|
||||
let id = sketch_store::active(s).ok_or_else(|| EngineError::Other("no sketch is being edited".into()))?;
|
||||
let st = s.model.state();
|
||||
let ss = st.sketch(id).ok_or_else(|| EngineError::Other("the sketch is not evaluated".into()))?;
|
||||
let ci = ss.sketch.curve_index(axis).ok_or_else(|| bad(cmd, format!("unknown curve `{axis}`")))?;
|
||||
@@ -422,48 +421,47 @@ fn fit_section(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if s.model.state().body(&body).is_none() {
|
||||
return Err(bad(cmd, format!("no body `{body}`")));
|
||||
}
|
||||
let id = s.active_sketch.ok_or_else(|| EngineError::Other("no sketch is being edited".into()))?;
|
||||
let mut doc = (*s.doc).clone();
|
||||
let mut sk = doc.sketch(id)?.clone();
|
||||
let l = add_link(s, &doc, id, &mut sk, LinkKind::Intersect, LinkSource::Body { body }).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
let curves: Vec<String> = sk.link_curves(&l).iter().filter_map(|c| sk.curves.get(*c).map(|c| c.id.clone())).collect();
|
||||
sk.break_link(&l)?;
|
||||
*doc.sketch_mut(id)? = sk;
|
||||
*s.doc_mut() = doc;
|
||||
let id = sketch_store::active(s).ok_or_else(|| EngineError::Other("no sketch is being edited".into()))?;
|
||||
let curves = sketch_store::update(s, id, |s, doc, sk| {
|
||||
let l = add_link(s, doc, id, sk, LinkKind::Intersect, LinkSource::Body { body }).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
let curves: Vec<String> = sk.link_curves(&l).iter().filter_map(|c| sk.curves.get(*c).map(|c| c.id.clone())).collect();
|
||||
sk.break_link(&l)?;
|
||||
Ok(curves)
|
||||
})?;
|
||||
Ok(json!({"curves": curves}))
|
||||
}
|
||||
|
||||
fn break_link(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.break_link";
|
||||
let id = s.active_sketch.ok_or_else(|| EngineError::Other("no sketch is being edited".into()))?;
|
||||
let mut doc = (*s.doc).clone();
|
||||
let sk = doc.sketch_mut(id)?;
|
||||
let mut links: Vec<String> = Vec::new();
|
||||
if let Some(l) = str_(p, "link") {
|
||||
if sk.link(l).is_none() {
|
||||
return Err(bad(cmd, format!("no link `{l}`")));
|
||||
let id = sketch_store::active(s).ok_or_else(|| EngineError::Other("no sketch is being edited".into()))?;
|
||||
let links = sketch_store::update(s, id, |_, _, sk| {
|
||||
let mut links: Vec<String> = Vec::new();
|
||||
if let Some(l) = str_(p, "link") {
|
||||
if sk.link(l).is_none() {
|
||||
return Err(bad(cmd, format!("no link `{l}`")));
|
||||
}
|
||||
links.push(l.to_string());
|
||||
}
|
||||
links.push(l.to_string());
|
||||
}
|
||||
for e in crate::params::string_list(p, "entities") {
|
||||
let l = sk
|
||||
.curves
|
||||
.iter()
|
||||
.find(|c| c.id == e)
|
||||
.and_then(|c| c.link.clone())
|
||||
.or_else(|| sk.points.iter().find(|q| q.id == e).and_then(|q| q.link.clone()))
|
||||
.ok_or_else(|| bad(cmd, format!("`{e}` is not linked geometry")))?;
|
||||
if !links.contains(&l) {
|
||||
links.push(l);
|
||||
for e in crate::params::string_list(p, "entities") {
|
||||
let l = sk
|
||||
.curves
|
||||
.iter()
|
||||
.find(|c| c.id == e)
|
||||
.and_then(|c| c.link.clone())
|
||||
.or_else(|| sk.points.iter().find(|q| q.id == e).and_then(|q| q.link.clone()))
|
||||
.ok_or_else(|| bad(cmd, format!("`{e}` is not linked geometry")))?;
|
||||
if !links.contains(&l) {
|
||||
links.push(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
if links.is_empty() {
|
||||
return Err(bad(cmd, "give `link` or `entities`"));
|
||||
}
|
||||
for l in &links {
|
||||
sk.break_link(l)?;
|
||||
}
|
||||
*s.doc_mut() = doc;
|
||||
if links.is_empty() {
|
||||
return Err(bad(cmd, "give `link` or `entities`"));
|
||||
}
|
||||
for l in &links {
|
||||
sk.break_link(l)?;
|
||||
}
|
||||
Ok(links)
|
||||
})?;
|
||||
Ok(json!({"broken": links}))
|
||||
}
|
||||
|
||||
@@ -505,7 +503,7 @@ fn curve_intersection_arg(v: &Value, cmd: &str) -> Result<Option<(String, String
|
||||
/// two lines, circles or arcs (and `"vertex:x,y,z"` is the vertex form). Everything else is left
|
||||
/// as it is. The links are added to the active sketch in the session's document.
|
||||
pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result<Value> {
|
||||
let Some(id) = s.active_sketch else { return Ok(p.clone()) };
|
||||
let Some(id) = sketch_store::active(s) else { return Ok(p.clone()) };
|
||||
let mut out = p.clone();
|
||||
let mut doc: Option<(Document, Sketch)> = None;
|
||||
let fix = |v: &mut Value, s: &Session, doc: &mut Option<(Document, Sketch)>| -> Result<()> {
|
||||
@@ -535,7 +533,7 @@ pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result<Value
|
||||
}
|
||||
if doc.is_none() {
|
||||
let d = (*s.doc).clone();
|
||||
let mut sk = d.sketch(id)?.clone();
|
||||
let mut sk = sketch_store::read(&d, id)?.clone();
|
||||
refresh(s, &d, id, &mut sk);
|
||||
*doc = Some((d, sk));
|
||||
}
|
||||
@@ -603,7 +601,7 @@ pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result<Value
|
||||
}
|
||||
}
|
||||
if let Some((mut d, sk)) = doc {
|
||||
*d.sketch_mut(id)? = sk;
|
||||
sketch_store::write(&mut d, id, sk)?;
|
||||
*s.doc_mut() = d;
|
||||
}
|
||||
Ok(out)
|
||||
@@ -627,7 +625,7 @@ pub(super) fn project_internal_faces(s: &mut Session, id: u64, body: &str, indic
|
||||
let plane = sketch_plane(&doc, id, &state)?;
|
||||
let names = state.body(body).map(solidcraft_doc::naming::face_names).unwrap_or_default();
|
||||
|
||||
let mut sk = doc.sketch(id)?.clone();
|
||||
let mut sk = sketch_store::read(&doc, id)?.clone();
|
||||
let mut links = Vec::with_capacity(indices.len());
|
||||
for &index in indices {
|
||||
let resolved = solidcraft_doc::project::resolve_face_indices(&state, &plane, LinkKind::Project, body, &[index])?;
|
||||
@@ -637,7 +635,7 @@ pub(super) fn project_internal_faces(s: &mut Session, id: u64, body: &str, indic
|
||||
}
|
||||
links.push(link);
|
||||
}
|
||||
*doc.sketch_mut(id)? = sk;
|
||||
sketch_store::write(&mut doc, id, sk)?;
|
||||
*s.doc_mut() = doc;
|
||||
Ok(links)
|
||||
}
|
||||
@@ -660,13 +658,13 @@ pub(super) fn auto_project_face(s: &mut Session, id: u64, _at: Vec3) -> Result<O
|
||||
let state = solidcraft_doc::state_in_face_anchor_frame(&doc, &raw, &anchor, author)?;
|
||||
let plane = doc.resolve_face_anchor(&anchor, author, &raw)?;
|
||||
let resolved = solidcraft_doc::project::resolve_anchored_face(&doc, &state, &plane, LinkKind::Project, &anchor)?;
|
||||
let mut sk = doc.sketch(id)?.clone();
|
||||
let mut sk = sketch_store::read(&doc, id)?.clone();
|
||||
let link = sk.add_link_with_wires(LinkKind::Project, resolved.source, &resolved.geom, &resolved.wires)?;
|
||||
if let Some(link) = sk.links.iter_mut().find(|entry| entry.id == link) {
|
||||
link.face_name = Some(anchor.target.name.clone());
|
||||
link.auto_face = true;
|
||||
}
|
||||
*doc.sketch_mut(id)? = sk;
|
||||
sketch_store::write(&mut doc, id, sk)?;
|
||||
*s.doc_mut() = doc;
|
||||
Ok(Some(link))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
//! The one door the sketch commands go through to find and keep the sketch they work on (W38.0,
|
||||
//! #5425). Today every sketch is a design feature; a drawing session (W38a/b) adds sheet and view
|
||||
//! sketches behind these same calls, so the commands don't change again. A source scan
|
||||
//! (`each_sketch_command_goes_through_the_store`) keeps every sketch read and write in the
|
||||
//! commands here, apart from the design's own, each marked `// design-only:`.
|
||||
|
||||
use serde_json::Value;
|
||||
use solidcraft_doc::{Document, FeatureKind};
|
||||
use solidcraft_sketch::Sketch;
|
||||
|
||||
use crate::params::bad;
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
/// The sketch a command acts on: its `sketch` param (id or name), or the sketch being edited.
|
||||
pub(super) fn target(s: &Session, p: &Value, cmd: &str) -> Result<u64> {
|
||||
if let Some(v) = p.get("sketch") {
|
||||
let key = match v {
|
||||
Value::Number(n) => n.to_string(),
|
||||
Value::String(x) => x.clone(),
|
||||
_ => return Err(bad(cmd, "`sketch` must be an id or a name")),
|
||||
};
|
||||
let f = s.doc.find_feature(&key).ok_or_else(|| bad(cmd, format!("no sketch `{key}`")))?;
|
||||
if !matches!(f.kind, FeatureKind::Sketch { .. }) {
|
||||
return Err(bad(cmd, format!("`{key}` is not a sketch")));
|
||||
}
|
||||
return Ok(f.id);
|
||||
}
|
||||
s.active_sketch.ok_or_else(|| EngineError::Other("no sketch is being edited".into()))
|
||||
}
|
||||
|
||||
/// The sketch being edited, if any; each command keeps its own refusal when there is none.
|
||||
pub(super) fn active(s: &Session) -> Option<u64> {
|
||||
s.active_sketch
|
||||
}
|
||||
|
||||
/// Sketch `id`, to read.
|
||||
pub(super) fn read(doc: &Document, id: u64) -> Result<&Sketch> {
|
||||
Ok(doc.sketch(id)?)
|
||||
}
|
||||
|
||||
/// Keep `sk` as sketch `id`.
|
||||
pub(super) fn write(doc: &mut Document, id: u64, sk: Sketch) -> Result<()> {
|
||||
*doc.sketch_mut(id)? = sk;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Change sketch `id` on a copy of the document, then keep both: the link and text commands'
|
||||
/// edit. As those commands always have, it neither solves the sketch nor runs [`checked`].
|
||||
pub(super) fn update<T>(s: &mut Session, id: u64, f: impl FnOnce(&Session, &Document, &mut Sketch) -> Result<T>) -> Result<T> {
|
||||
let mut doc = (*s.doc).clone();
|
||||
let mut sk = read(&doc, id)?.clone();
|
||||
let out = f(s, &doc, &mut sk)?;
|
||||
write(&mut doc, id, sk)?;
|
||||
*s.doc_mut() = doc;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The checks an edited sketch passes before it is kept.
|
||||
pub(super) fn checked(sk: &Sketch) -> Result<()> {
|
||||
sk.validate_periodic_splines()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// Each sketch command finds and keeps its sketch through this store (W38.0). Outside it, the
|
||||
/// command sources (non-test code) reach a sketch directly only on a line marked
|
||||
/// `// design-only:` in the files that work on the design's own sketches.
|
||||
#[test]
|
||||
fn each_sketch_command_goes_through_the_store() {
|
||||
const PATTERNS: [&str; 4] = [".sketch_mut(", "doc.sketch(", " d.sketch(", "active_sketch.ok_or"];
|
||||
// A motion trace's wire, a joint's curve and Redefine Sketch Plane: the design's sketches.
|
||||
const DESIGN_ONLY: [&str; 3] = ["browser.rs", "joints_inspect.rs", "motion_trace.rs"];
|
||||
let mut stack = vec![std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/cmd")];
|
||||
let (mut offenders, mut scanned) = (Vec::new(), 0);
|
||||
while let Some(dir) = stack.pop() {
|
||||
for entry in std::fs::read_dir(&dir).unwrap().flatten() {
|
||||
let path = entry.path();
|
||||
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or_default().to_string();
|
||||
if path.is_dir() {
|
||||
stack.push(path);
|
||||
continue;
|
||||
}
|
||||
if !name.ends_with(".rs") || name.ends_with("_tests.rs") || name == "sketch_store.rs" {
|
||||
continue;
|
||||
}
|
||||
scanned += 1;
|
||||
// A Windows checkout's CRLF lines, read as written, so the split below finds the tests.
|
||||
let text = std::fs::read_to_string(&path).unwrap().replace("\r\n", "\n");
|
||||
let lines: Vec<&str> = text.split("\n#[cfg(test)]\nmod ").next().unwrap_or_default().lines().collect();
|
||||
for (n, line) in lines.iter().enumerate() {
|
||||
let marked = n > 0 && lines[n - 1].trim_start().starts_with("// design-only:") && DESIGN_ONLY.contains(&name.as_str());
|
||||
if !line.trim_start().starts_with("//") && !marked && PATTERNS.iter().any(|p| line.contains(p)) {
|
||||
offenders.push(format!("{}:{}: {}", path.display(), n + 1, line.trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(scanned > 50, "the command sources were scanned ({scanned} files)");
|
||||
assert!(offenders.is_empty(), "a sketch read or written outside cmd::sketch_store; go through it:\n{}", offenders.join("\n"));
|
||||
}
|
||||
}
|
||||
@@ -862,6 +862,7 @@ impl Session {
|
||||
/// Show the sketch a live frame changed, leaving the features after it as they were last
|
||||
/// evaluated (marked stale); a full `refresh` when the model cannot show it that way.
|
||||
fn refresh_live(&mut self, edit: LiveEdit) {
|
||||
// design-only: a live drag shows the design's sketch in the model.
|
||||
let sketch = self.doc.sketch(edit.sketch).ok().cloned();
|
||||
if sketch.is_some_and(|sketch| self.model.show_live_sketch(edit.sketch, sketch, edit.report)) {
|
||||
self.revision += 1;
|
||||
@@ -910,6 +911,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
for (id, sketch) in updated {
|
||||
// design-only: a parameter edit rewrites the design's pattern-quantity sketches.
|
||||
*self.doc_mut().sketch_mut(id)? = sketch;
|
||||
}
|
||||
self.refresh();
|
||||
@@ -1064,6 +1066,7 @@ impl Session {
|
||||
_ => None,
|
||||
};
|
||||
let Some(sk) = named.or(self.active_sketch).or(before.active_sketch) else { return v };
|
||||
// design-only: a command's `created` compares two snapshots of the design.
|
||||
let Ok(after) = self.doc.sketch(sk) else { return v };
|
||||
out.insert("created".into(), cmd::sketch_created(before.doc.sketch(sk).ok(), after));
|
||||
v
|
||||
|
||||
Reference in New Issue
Block a user