Constraint glyphs: one per constrained entity, beside it, readable
Fixes #43 - Two-entity constraints (parallel, tangent, equal, ...) get a glyph on each entity. - Glyphs sit beside their entity (offset along its screen normal), step along it when crowded, with a faint leader when pushed away; 15 px boxes with the toolbar's constraint icons (H/V stay letters), coloured from the theme's glyph tokens in both themes. - Hovering a glyph lights both of its glyphs and its curves/points and names the constraint. - Removes the old duplicate glyph drawer in viewport.rs (it drew a second, green set). - Scenario expect key `glyphs` ({constraint id: count}, none overlapping) and the scenario constraint_glyphs_both_entities. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT
This commit is contained in:
1 parent
eac7306d59
commit
a5729f1f97
4 files changed
+170
-85
No files matched your search
@@ -518,7 +518,7 @@ fn approx(got: f64, want: &Value, what: &str) -> Result<(), String> {
|
||||
/// Checks: `bodies` (count), `volume` / `area` (total, number or [value, rel tol]), `faces`,
|
||||
/// `features` (count), `errors` (timeline features in error, default 0), `dialog` (null: none
|
||||
/// open, or a word its kind must contain), `tool` (null or command id), `sketching` (bool),
|
||||
/// `selection` (count), `param` ({name: value}), `sketch_status` (solved|…), `dof`, `query`
|
||||
/// `selection` (count), `glyphs` ({constraint id: count}, none overlapping), `param` ({name: value}), `sketch_status` (solved|…), `dof`, `query`
|
||||
/// ({command, params?, path, value}: a command's result).
|
||||
pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
|
||||
let doc = h.call("document.inspect", json!({"measure": true}))["result"].clone();
|
||||
@@ -656,6 +656,21 @@ pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
|
||||
return Err(format!("tool: got {}, want {v}", ui["tool"]));
|
||||
}
|
||||
}
|
||||
"glyphs" => {
|
||||
// {constraint id: glyphs drawn for it}; glyphs never overlap one another.
|
||||
let g = crate::sketch_tools::glyph_positions();
|
||||
for (id, n) in v.as_object().ok_or("glyphs takes {constraint id: count}")? {
|
||||
let got = g.iter().filter(|(x, _)| x == id).count();
|
||||
if n.as_u64() != Some(got as u64) {
|
||||
return Err(format!("glyphs of {id}: got {got}, want {n} (drawn: {g:?})"));
|
||||
}
|
||||
}
|
||||
for (i, (a, p)) in g.iter().enumerate() {
|
||||
if let Some((b, _)) = g.iter().skip(i + 1).find(|(_, q)| q.distance(*p) < 14.0) {
|
||||
return Err(format!("glyphs of {a} and {b} overlap at {p:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
"sketching" => {
|
||||
if doc["active_sketch"].is_null() == v.as_bool().unwrap_or(false) {
|
||||
return Err(format!("sketching: want {v}, active sketch {}", doc["active_sketch"]));
|
||||
|
||||
@@ -291,7 +291,7 @@ pub fn refine_snap(
|
||||
|
||||
/// The constraint glyph under a screen position (its constraint id).
|
||||
pub fn glyph_at(pos: Pos2) -> Option<String> {
|
||||
GLYPHS.with(|g| g.borrow().iter().find(|(_, c)| c.distance(pos) <= 7.0).map(|(id, _)| id.clone()))
|
||||
GLYPHS.with(|g| g.borrow().iter().find(|(_, c)| c.distance(pos) <= GLYPH * 0.6).map(|(id, _)| id.clone()))
|
||||
}
|
||||
|
||||
/// Constraint glyphs drawn last frame: (constraint id, screen centre).
|
||||
@@ -304,7 +304,7 @@ pub fn click_glyph(app: &mut SolveApp, _proj: &Proj, pos: Pos2) -> bool {
|
||||
if app.session.active_sketch.is_none() {
|
||||
return false;
|
||||
}
|
||||
let hit = GLYPHS.with(|g| g.borrow().iter().find(|(_, c)| c.distance(pos) <= 7.0).map(|(id, _)| id.clone()));
|
||||
let hit = GLYPHS.with(|g| g.borrow().iter().find(|(_, c)| c.distance(pos) <= GLYPH * 0.6).map(|(id, _)| id.clone()));
|
||||
let used = hit.is_some();
|
||||
PICKED.with(|p| *p.borrow_mut() = hit.clone());
|
||||
if let Some(id) = hit {
|
||||
@@ -891,7 +891,10 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
|
||||
|
||||
/// Constraint glyphs of the active sketch: a small badge per constraint at its anchor
|
||||
/// (conflicting ones red).
|
||||
fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
/// Glyph size and spacing (screen pixels).
|
||||
const GLYPH: f32 = 15.0;
|
||||
|
||||
fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj, hover: Option<Pos2>) {
|
||||
let Some(sid) = active_sketch(app) else { return };
|
||||
let st = app.session.world_state();
|
||||
let Some(ss) = st.sketch(sid) else { return };
|
||||
@@ -900,87 +903,172 @@ fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
return;
|
||||
}
|
||||
let tk = crate::theme::Tokens::get();
|
||||
let mut placed: Vec<Pos2> = Vec::new();
|
||||
let picked = PICKED.with(|p| p.borrow().clone());
|
||||
let mut drawn: Vec<(String, Pos2)> = Vec::new();
|
||||
// Where each glyph goes: beside its entity (to its left, away from the line), every entity
|
||||
// of a two-entity constraint getting one; glyphs that would overlap step along.
|
||||
let mut placed: Vec<(String, Pos2, Pos2)> = Vec::new();
|
||||
for c in &ss.sketch.constraints {
|
||||
if c.kind.is_dimension() {
|
||||
continue;
|
||||
}
|
||||
let Some(at) = glyph_anchor(&ss.sketch, &c.kind) else { continue };
|
||||
let Some(mut sp) = proj.to_screen(ss.plane.to_world(at)) else { continue };
|
||||
sp += egui::vec2(9.0, -9.0);
|
||||
// Keep glyphs at one spot from overlapping.
|
||||
while placed.iter().any(|q| q.distance(sp) < 12.0) {
|
||||
sp += egui::vec2(13.0, 0.0);
|
||||
for (at, dir) in glyph_anchors(&ss.sketch, &c.kind) {
|
||||
let Some(base) = proj.to_screen(ss.plane.to_world(at)) else { continue };
|
||||
// The entity's direction on screen, and its side.
|
||||
let side = match dir.and_then(|d| proj.to_screen(ss.plane.to_world(at + d))) {
|
||||
Some(q) => {
|
||||
let t = (q - base).normalized();
|
||||
if t.length() > 0.5 { egui::vec2(-t.y, t.x) } else { egui::vec2(0.7, -0.7) }
|
||||
}
|
||||
None => egui::vec2(0.7, -0.7),
|
||||
};
|
||||
let step = if dir.is_some() { egui::vec2(side.y, -side.x) } else { egui::vec2(1.0, 0.0) };
|
||||
let mut sp = base + side * (GLYPH * 0.9);
|
||||
for _ in 0..12 {
|
||||
if !placed.iter().any(|(_, q, _)| q.distance(sp) < GLYPH + 1.0) {
|
||||
break;
|
||||
}
|
||||
sp += step * (GLYPH + 2.0);
|
||||
}
|
||||
placed.push((c.id.clone(), sp, base));
|
||||
}
|
||||
placed.push(sp);
|
||||
let failing = ss.report.failing.contains(&c.id);
|
||||
let selected = picked.as_deref() == Some(c.id.as_str())
|
||||
|| app.session.selection.iter().any(|s| matches!(s, solvecraft_engine::Sel::SketchConstraint { id } if *id == c.id));
|
||||
}
|
||||
let hovered = hover.and_then(|h| placed.iter().find(|(_, q, _)| q.distance(h) <= GLYPH * 0.6).map(|(id, _, _)| id.clone()));
|
||||
// The hovered (or selected) constraint's entities lit up.
|
||||
if let Some(id) = hovered.as_ref().or(picked.as_ref())
|
||||
&& let Some(c) = ss.sketch.constraints.iter().find(|c| &c.id == id)
|
||||
{
|
||||
let (cs, ps) = constraint_entities(&c.kind);
|
||||
for ci in cs {
|
||||
let pts: Vec<Pos2> = ss.sketch.polyline(ci).iter().filter_map(|q| proj.to_screen(ss.plane.to_world(*q))).collect();
|
||||
painter.add(egui::Shape::line(pts, Stroke::new(3.0, tk.accent.gamma_multiply(0.7))));
|
||||
}
|
||||
for pi in ps {
|
||||
if let Some(q) = ss.sketch.point(pi).and_then(|q| proj.to_screen(ss.plane.to_world(q))) {
|
||||
painter.circle_filled(q, 4.5, tk.accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut drawn: Vec<(String, Pos2)> = Vec::new();
|
||||
for (id, sp, base) in &placed {
|
||||
let Some(c) = ss.sketch.constraints.iter().find(|c| &c.id == id) else { continue };
|
||||
let failing = ss.report.failing.contains(id);
|
||||
let selected = picked.as_deref() == Some(id.as_str())
|
||||
|| app.session.selection.iter().any(|s| matches!(s, solvecraft_engine::Sel::SketchConstraint { id: x } if x == id));
|
||||
let lit = selected || hovered.as_deref() == Some(id.as_str());
|
||||
let col = if failing {
|
||||
tk.error
|
||||
} else if selected {
|
||||
} else if lit {
|
||||
tk.accent
|
||||
} else {
|
||||
tk.text_dim
|
||||
tk.glyph_text
|
||||
};
|
||||
drawn.push((c.id.clone(), sp));
|
||||
let r = egui::Rect::from_center_size(sp, egui::vec2(12.0, 12.0));
|
||||
painter.rect_filled(r, 2.0, tk.panel.gamma_multiply(0.85));
|
||||
painter.rect_stroke(r, 2.0, Stroke::new(0.8, col), egui::StrokeKind::Inside);
|
||||
painter.text(sp, egui::Align2::CENTER_CENTER, glyph_letter(c.kind.name()), egui::FontId::proportional(9.0), col);
|
||||
// A faint leader back to the entity when the glyph was pushed away from it.
|
||||
if sp.distance(*base) > GLYPH * 1.6 {
|
||||
painter.line_segment([*base, *sp], Stroke::new(0.8, tk.text_dim.gamma_multiply(0.6)));
|
||||
}
|
||||
let r = egui::Rect::from_center_size(*sp, egui::vec2(GLYPH, GLYPH));
|
||||
painter.rect_filled(r, 3.0, if lit { tk.accent_soft } else { tk.glyph_bg });
|
||||
painter.rect_stroke(r, 3.0, Stroke::new(1.0, if failing || lit { col } else { tk.glyph_edge }), egui::StrokeKind::Inside);
|
||||
match glyph_icon(c.kind.name()) {
|
||||
Some(icon) => crate::icons::paint(painter, r.shrink(2.0), icon, col, tk.glyph_bg, col),
|
||||
None => {
|
||||
painter.text(*sp, egui::Align2::CENTER_CENTER, glyph_letter(c.kind.name()), egui::FontId::proportional(10.5), col);
|
||||
}
|
||||
}
|
||||
drawn.push((id.clone(), *sp));
|
||||
}
|
||||
if let (Some(id), Some(h)) = (hovered, hover)
|
||||
&& let Some(c) = ss.sketch.constraints.iter().find(|c| c.id == id)
|
||||
{
|
||||
let label = format!("{} ({id})", c.kind.name());
|
||||
let at = h + egui::vec2(12.0, 12.0);
|
||||
let g = painter.layout_no_wrap(label, egui::FontId::proportional(11.0), tk.text);
|
||||
painter.rect_filled(egui::Rect::from_min_size(at, g.size()).expand(3.0), 3.0, tk.panel);
|
||||
painter.galley(at, g, tk.text);
|
||||
}
|
||||
GLYPHS.with(|g| *g.borrow_mut() = drawn);
|
||||
}
|
||||
|
||||
/// Toolbar icons used as glyphs (H and V stay letters, which tell them apart).
|
||||
fn glyph_icon(name: &str) -> Option<&'static str> {
|
||||
Some(match name {
|
||||
"Coincident" => "c_coincident",
|
||||
"Parallel" => "c_parallel",
|
||||
"Perpendicular" => "c_perpendicular",
|
||||
"Collinear" => "c_collinear",
|
||||
"Tangent" => "c_tangent",
|
||||
"Smooth" => "c_smooth",
|
||||
"Equal" => "c_equal",
|
||||
"Concentric" => "c_concentric",
|
||||
"MidPoint" => "c_midpoint",
|
||||
"Symmetry" => "c_symmetry",
|
||||
"Fix" => "c_fix",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The curves and points a constraint ties together.
|
||||
fn constraint_entities(k: &solvecraft_engine::sketch::ConstraintKind) -> (Vec<usize>, Vec<usize>) {
|
||||
use solvecraft_engine::sketch::ConstraintKind::*;
|
||||
match *k {
|
||||
Coincident { p, q } | HorizontalPoints { p, q } | VerticalPoints { p, q } => (vec![], vec![p, q]),
|
||||
Fix { p } => (vec![], vec![p]),
|
||||
Midpoint { p, l } => (vec![l], vec![p]),
|
||||
PointOnCurve { p, c } => (vec![c], vec![p]),
|
||||
Symmetric { p, q, l } => (vec![l], vec![p, q]),
|
||||
Horizontal { l } | Vertical { l } => (vec![l], vec![]),
|
||||
Parallel { a, b }
|
||||
| Perpendicular { a, b }
|
||||
| Collinear { a, b }
|
||||
| Equal { a, b }
|
||||
| Concentric { a, b }
|
||||
| Tangent { a, b }
|
||||
| Smooth { a, b } => (vec![a, b], vec![]),
|
||||
_ => (vec![], vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
fn glyph_letter(name: &str) -> &'static str {
|
||||
match name {
|
||||
"Coincident" => "\u{25cf}",
|
||||
"Horizontal" => "H",
|
||||
"Vertical" => "V",
|
||||
"Parallel" => "//",
|
||||
"Perpendicular" => "\u{22a5}",
|
||||
"Collinear" => "\u{2261}",
|
||||
"Tangent" => "T",
|
||||
"Smooth" => "G2",
|
||||
"Equal" => "=",
|
||||
"Concentric" => "\u{25ce}",
|
||||
"MidPoint" => "M",
|
||||
"Symmetry" => "[]",
|
||||
"Fix" => "\u{1f512}",
|
||||
_ => "?",
|
||||
}
|
||||
}
|
||||
|
||||
fn glyph_anchor(sk: &solvecraft_engine::sketch::Sketch, k: &solvecraft_engine::sketch::ConstraintKind) -> Option<Vec2> {
|
||||
/// Where a constraint's glyphs go: one per entity (the middle of a curve, with its direction
|
||||
/// there; a point, without).
|
||||
fn glyph_anchors(sk: &solvecraft_engine::sketch::Sketch, k: &solvecraft_engine::sketch::ConstraintKind) -> Vec<(Vec2, Option<Vec2>)> {
|
||||
use solvecraft_engine::sketch::ConstraintKind::*;
|
||||
// Half way along the curve.
|
||||
let mid = |c: usize| -> Option<Vec2> {
|
||||
// Half way along the curve, and the direction there.
|
||||
let mid = |c: usize| -> Option<(Vec2, Option<Vec2>)> {
|
||||
let poly = sk.polyline(c);
|
||||
let total: f64 = poly.windows(2).map(|w| w[0].dist(w[1])).sum();
|
||||
let mut acc = 0.0;
|
||||
for w in poly.windows(2) {
|
||||
let l = w[0].dist(w[1]);
|
||||
if acc + l >= total * 0.5 && l > 0.0 {
|
||||
return Some(w[0].lerp(w[1], (total * 0.5 - acc) / l));
|
||||
return Some((w[0].lerp(w[1], (total * 0.5 - acc) / l), (w[1] - w[0]).normalized()));
|
||||
}
|
||||
acc += l;
|
||||
}
|
||||
poly.first().copied()
|
||||
poly.first().map(|p| (*p, None))
|
||||
};
|
||||
let pt = |p: usize| sk.point(p).map(|q| (q, None));
|
||||
match *k {
|
||||
Coincident { p, .. } | Fix { p } | Midpoint { p, .. } | PointOnCurve { p, .. } => sk.point(p),
|
||||
HorizontalPoints { p, q } | VerticalPoints { p, q } | Symmetric { p, q, .. } => Some((sk.point(p)? + sk.point(q)?) * 0.5),
|
||||
Horizontal { l } | Vertical { l } => mid(l),
|
||||
Parallel { a, .. }
|
||||
| Perpendicular { a, .. }
|
||||
| Collinear { a, .. }
|
||||
| Equal { a, .. }
|
||||
| Concentric { a, .. }
|
||||
| Tangent { a, .. }
|
||||
| Smooth { a, .. } => mid(a),
|
||||
_ => None,
|
||||
Coincident { p, .. } | Fix { p } | Midpoint { p, .. } | PointOnCurve { p, .. } => pt(p).into_iter().collect(),
|
||||
HorizontalPoints { p, q } | VerticalPoints { p, q } | Symmetric { p, q, .. } => {
|
||||
sk.point(p).zip(sk.point(q)).map(|(a, b)| ((a + b) * 0.5, None)).into_iter().collect()
|
||||
}
|
||||
Horizontal { l } | Vertical { l } => mid(l).into_iter().collect(),
|
||||
Parallel { a, b }
|
||||
| Perpendicular { a, b }
|
||||
| Collinear { a, b }
|
||||
| Equal { a, b }
|
||||
| Concentric { a, b }
|
||||
| Tangent { a, b }
|
||||
| Smooth { a, b } => [mid(a), mid(b)].into_iter().flatten().collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1002,7 +1090,7 @@ fn snap_hint(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
/// Per frame, after the viewport is drawn: constraint glyphs and the text-entry box.
|
||||
pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &Proj) {
|
||||
SHIFT.with(|c| c.set(ui.input(|i| i.modifiers.shift)));
|
||||
glyphs(app, painter, proj);
|
||||
glyphs(app, painter, proj, ui.input(|i| i.pointer.hover_pos()));
|
||||
snap_hint(app, painter, proj);
|
||||
combs(app, painter, proj);
|
||||
if DEFERRED.with(|d| d.replace(false)) {
|
||||
|
||||
@@ -6,7 +6,7 @@ use serde_json::json;
|
||||
use solvecraft_engine::Sel;
|
||||
use solvecraft_engine::geom::{Vec2, Vec3};
|
||||
use solvecraft_engine::render::{Camera, Mat4, Rgb};
|
||||
use solvecraft_engine::sketch::{ConstraintKind, CurveKind};
|
||||
use solvecraft_engine::sketch::CurveKind;
|
||||
use solvecraft_engine::view::{colors, grid_step, sketch_consumed, sketch_lines};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
@@ -1812,7 +1812,7 @@ fn hover_highlight(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sketch points, dimension labels and constraint glyphs of the active sketch.
|
||||
/// Sketch points and the status of the active sketch (constraint glyphs: `sketch_tools`).
|
||||
fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
let t = Tokens::get();
|
||||
let st = app.session.world_state();
|
||||
@@ -1840,9 +1840,6 @@ fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
painter.rect_filled(Rect::from_center_size(sp, vec2(5.0, 5.0)), 0.0, c);
|
||||
}
|
||||
}
|
||||
for c in sk.constraints.iter().filter(|c| !c.kind.is_dimension() && !sk.view.hide_constraints) {
|
||||
glyph(painter, proj, ss, &c.kind);
|
||||
}
|
||||
// Sketch status.
|
||||
if let Some(r) = app.viewport.rect {
|
||||
let msg = if !ss.report.ok() {
|
||||
@@ -1857,35 +1854,6 @@ fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
}
|
||||
}
|
||||
|
||||
fn line_mid(sk: &solvecraft_engine::sketch::Sketch, l: usize) -> Option<(Vec2, Vec2)> {
|
||||
match sk.curves.get(l)?.kind {
|
||||
CurveKind::Line { a, b } => Some((sk.point(a)?, sk.point(b)?)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Small constraint markers next to the constrained geometry.
|
||||
fn glyph(painter: &egui::Painter, proj: &Proj, ss: &solvecraft_engine::doc::SolvedSketch, k: &ConstraintKind) {
|
||||
use ConstraintKind::*;
|
||||
let sk = &ss.sketch;
|
||||
let (anchor, label) = match *k {
|
||||
Horizontal { l } => (line_mid(sk, l).map(|(a, b)| (a + b) * 0.5), "H"),
|
||||
Vertical { l } => (line_mid(sk, l).map(|(a, b)| (a + b) * 0.5), "V"),
|
||||
Parallel { a, .. } => (line_mid(sk, a).map(|(p, q)| (p + q) * 0.5), "//"),
|
||||
Perpendicular { a, .. } => (line_mid(sk, a).map(|(p, q)| (p + q) * 0.5), "L"),
|
||||
Tangent { a, .. } => (line_mid(sk, a).map(|(p, q)| (p + q) * 0.5).or_else(|| sk.center(a)), "T"),
|
||||
Equal { a, .. } => (line_mid(sk, a).map(|(p, q)| (p + q) * 0.5).or_else(|| sk.center(a)), "="),
|
||||
Fix { p } => (sk.point(p), "F"),
|
||||
_ => (None, ""),
|
||||
};
|
||||
let Some(a) = anchor.and_then(|a| proj.to_screen(ss.plane.to_world(a))) else { return };
|
||||
let r = Rect::from_center_size(a + vec2(10.0, -10.0), vec2(13.0, 13.0));
|
||||
let t = Tokens::get();
|
||||
painter.rect_filled(r, 2.0, t.glyph_bg);
|
||||
painter.rect_stroke(r, 2.0, Stroke::new(1.0, t.glyph_edge), egui::StrokeKind::Inside);
|
||||
painter.text(r.center(), Align2::CENTER_CENTER, label, FontId::proportional(10.0), t.glyph_text);
|
||||
}
|
||||
|
||||
fn cpu_render(app: &mut SolveApp, ctx: &egui::Context, painter: &egui::Painter, rect: Rect, proj: &Proj) {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
scene_key(app).hash(&mut h);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{"call": "engine.execute", "params": {"command": "sketch.create", "params": {"plane": "XY"}}},
|
||||
{"call": "engine.execute", "params": {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [40, 20]}}},
|
||||
{"call": "engine.execute", "params": {"command": "sketch.circle.center", "params": {"center": [60, 10], "radius": 5}}},
|
||||
{"call": "engine.execute", "params": {"command": "sketch.line", "params": {"points": [[45, -10], [75, -10]]}}},
|
||||
{"call": "engine.execute", "params": {"command": "sketch.line", "params": {"points": [[45, 30], [75, 30]]}}},
|
||||
{"call": "engine.execute", "params": {"command": "sketch.constraint.parallel", "params": {"a": "l5", "b": "l6"}}},
|
||||
{"call": "engine.execute", "params": {"command": "sketch.constraint.tangent", "params": {"a": "l5", "b": "c1"}}},
|
||||
{"call": "engine.execute", "params": {"command": "sketch.constraint.tangent", "params": {"a": "l6", "b": "c1"}}},
|
||||
{"call": "engine.execute", "params": {"command": "sketch.constraint.equal", "params": {"a": "l5", "b": "l6"}}},
|
||||
{"call": "ui.view", "params": {"view": "top"}},
|
||||
{"call": "ui.view", "params": {"view": "fit"}},
|
||||
{"expect": {"glyphs": {"k1": 1, "k5": 2, "k6": 2, "k7": 2, "k8": 2}}}
|
||||
]
|
||||
Reference in new issue
Block a user