Insert Part dialog (SOLID > INSERT > Insert Fastener)
The standard parts library by type (with its ISO standard), size and length, the size's table values and a picture of the part; picking a hole's wall or rim seats it there with a rigid joint (the live preview shows it seated), without a pick it goes at the origin. Opens on FusionFastenersCommand (and parts.insert); OK runs FusionFastenersCommand. Its own icon. Insert's world picks (`at`, `point`, `direction`) go through the component frames like other picks. Tests for the library parsing and the insert parameters; checked in the app: an M3 socket head cap screw seated in a drilled hole with its joint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
4eff2146d2
commit
0a6f6c4422
6 files changed
+280
-2
No files matched your search
@@ -64,6 +64,7 @@ fn table(id: &str) -> &'static [(&'static str, K)] {
|
|||||||
"FusionCurvatureCombAnalysisCommand" | "FusionMinimumRadiusAnalysisCommand" | "FusionIsoCurveAnalysisCommand" => {
|
"FusionCurvatureCombAnalysisCommand" | "FusionMinimumRadiusAnalysisCommand" | "FusionIsoCurveAnalysisCommand" => {
|
||||||
&[("edges", Picks), ("faces", Picks)]
|
&[("edges", Picks), ("faces", Picks)]
|
||||||
}
|
}
|
||||||
|
"parts.insert" | "FusionFastenersCommand" => &[("at", Pick), ("point", Make), ("direction", Dir)],
|
||||||
"AppearanceCommand" => &[("faces", Picks), ("bodies", Body), ("body", Body)],
|
"AppearanceCommand" => &[("faces", Picks), ("bodies", Body), ("body", Body)],
|
||||||
"FusionMoveCommand" => &[("bodies", Body), ("translate", Dir), ("axis", Dir), ("origin", Make)],
|
"FusionMoveCommand" => &[("bodies", Body), ("translate", Dir), ("axis", Dir), ("origin", Make)],
|
||||||
"FusionCombineCommand" => &[("target", Body), ("tools", Body)],
|
"FusionCombineCommand" => &[("target", Body), ("tools", Body)],
|
||||||
|
|||||||
@@ -215,6 +215,8 @@ pub enum Kind {
|
|||||||
Appearance(crate::dialogs_appearance::Ap),
|
Appearance(crate::dialogs_appearance::Ap),
|
||||||
/// Contact sets, motion studies, exploded views, configurations (`dialogs_motion`).
|
/// Contact sets, motion studies, exploded views, configurations (`dialogs_motion`).
|
||||||
Motion(crate::dialogs_motion::Mo),
|
Motion(crate::dialogs_motion::Mo),
|
||||||
|
/// Insert Part from the standard parts library (`dialogs_parts`).
|
||||||
|
Part(crate::dialogs_parts::Pt),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The rest of the Hole dialog: placement, extents, tap type and drill point.
|
/// The rest of the Hole dialog: placement, extents, tap type and drill point.
|
||||||
@@ -413,7 +415,8 @@ impl Dialog {
|
|||||||
.or_else(|| crate::dialogs_sheet::start(app, id))
|
.or_else(|| crate::dialogs_sheet::start(app, id))
|
||||||
.or_else(|| crate::dialogs_plastic::start(app, id))
|
.or_else(|| crate::dialogs_plastic::start(app, id))
|
||||||
.or_else(|| crate::dialogs_appearance::start(app, id))
|
.or_else(|| crate::dialogs_appearance::start(app, id))
|
||||||
.or_else(|| crate::dialogs_motion::start(app, id))?;
|
.or_else(|| crate::dialogs_motion::start(app, id))
|
||||||
|
.or_else(|| crate::dialogs_parts::start(app, id))?;
|
||||||
Dialog::new(kind, inputs)
|
Dialog::new(kind, inputs)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -514,6 +517,7 @@ impl Dialog {
|
|||||||
Kind::Assembly(k) => return k.previews(),
|
Kind::Assembly(k) => return k.previews(),
|
||||||
Kind::Sheet(k) => return k.previews(),
|
Kind::Sheet(k) => return k.previews(),
|
||||||
Kind::Plastic(k) => return k.previews(),
|
Kind::Plastic(k) => return k.previews(),
|
||||||
|
Kind::Part(_) => return true,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
matches!(
|
matches!(
|
||||||
@@ -773,6 +777,7 @@ fn title(k: &Kind) -> &'static str {
|
|||||||
Kind::Plastic(k) => k.title(),
|
Kind::Plastic(k) => k.title(),
|
||||||
Kind::Appearance(_) => "APPEARANCE",
|
Kind::Appearance(_) => "APPEARANCE",
|
||||||
Kind::Motion(k) => k.title(),
|
Kind::Motion(k) => k.title(),
|
||||||
|
Kind::Part(_) => "INSERT PART",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1493,6 +1498,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
Kind::Plastic(k) => enter |= crate::dialogs_plastic::rows(app, ui, k, &d.inputs),
|
Kind::Plastic(k) => enter |= crate::dialogs_plastic::rows(app, ui, k, &d.inputs),
|
||||||
Kind::Appearance(k) => enter |= crate::dialogs_appearance::rows(app, ui, k, &mut d.inputs),
|
Kind::Appearance(k) => enter |= crate::dialogs_appearance::rows(app, ui, k, &mut d.inputs),
|
||||||
Kind::Motion(k) => enter |= crate::dialogs_motion::rows(app, ui, k, &mut d.inputs),
|
Kind::Motion(k) => enter |= crate::dialogs_motion::rows(app, ui, k, &mut d.inputs),
|
||||||
|
Kind::Part(k) => enter |= crate::dialogs_parts::rows(app, ui, k, &d.inputs),
|
||||||
Kind::ConfirmDelete { with, fail, .. } => {
|
Kind::ConfirmDelete { with, fail, .. } => {
|
||||||
if !with.is_empty() {
|
if !with.is_empty() {
|
||||||
row_label(ui, "Also deletes");
|
row_label(ui, "Also deletes");
|
||||||
@@ -2127,6 +2133,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
|||||||
Kind::Plastic(k) => return crate::dialogs_plastic::commands(app, k, &d.inputs, &d.extra),
|
Kind::Plastic(k) => return crate::dialogs_plastic::commands(app, k, &d.inputs, &d.extra),
|
||||||
Kind::Appearance(k) => return Ok(crate::dialogs_appearance::commands(k, &d.inputs)),
|
Kind::Appearance(k) => return Ok(crate::dialogs_appearance::commands(k, &d.inputs)),
|
||||||
Kind::Motion(k) => return crate::dialogs_motion::commands(app, k, &d.inputs),
|
Kind::Motion(k) => return crate::dialogs_motion::commands(app, k, &d.inputs),
|
||||||
|
Kind::Part(k) => return crate::dialogs_parts::commands(app, k, &d.inputs),
|
||||||
};
|
};
|
||||||
let mut params = params;
|
let mut params = params;
|
||||||
if let Value::Object(m) = &mut params {
|
if let Value::Object(m) = &mut params {
|
||||||
|
|||||||
@@ -26,7 +26,16 @@ const TYPE_LABELS: [&str; 7] = ["Rigid", "Revolute", "Slider", "Cylindrical", "P
|
|||||||
/// What a joint origin input accepts (snaps on faces, edges and vertices).
|
/// What a joint origin input accepts (snaps on faces, edges and vertices).
|
||||||
const SNAPS: u16 = FACES | EDGES | VERTICES;
|
const SNAPS: u16 = FACES | EDGES | VERTICES;
|
||||||
/// Commands whose effect is where occurrences sit (previewed on the placed model).
|
/// Commands whose effect is where occurrences sit (previewed on the placed model).
|
||||||
const MOVES: [&str; 6] = ["JointAssembleCmdNew", "JointAsBuiltCmd", "FusionMoveJointsCommand", "joint.edit", "joint.limits", "RigidGroupCmd"];
|
const MOVES: [&str; 8] = [
|
||||||
|
"JointAssembleCmdNew",
|
||||||
|
"JointAsBuiltCmd",
|
||||||
|
"FusionMoveJointsCommand",
|
||||||
|
"joint.edit",
|
||||||
|
"joint.limits",
|
||||||
|
"RigidGroupCmd",
|
||||||
|
"parts.insert",
|
||||||
|
"FusionFastenersCommand",
|
||||||
|
];
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Asm {
|
pub enum Asm {
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
//! Insert Part: the standard parts library (screws, bolts, nuts, washers, set screws, pins,
|
||||||
|
//! bearings) by family, size and length, with the size's table values and a picture of the part.
|
||||||
|
//! Picking a hole (its wall or rim) seats the part in it with a rigid joint; without one the part
|
||||||
|
//! goes at the origin. The live preview shows it placed.
|
||||||
|
|
||||||
|
use egui::{RichText, vec2};
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
use solvecraft_engine::{Sel, Session};
|
||||||
|
|
||||||
|
use crate::SolveApp;
|
||||||
|
use crate::dialogs::{Kind, combo, row_label};
|
||||||
|
use crate::selection::{EDGES, FACES, SelInput};
|
||||||
|
use crate::theme::Tokens;
|
||||||
|
|
||||||
|
/// One family of the library.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Family {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub standard: String,
|
||||||
|
/// Size names and their table rows.
|
||||||
|
pub sizes: Vec<(String, Value)>,
|
||||||
|
pub lengths: Option<Vec<f64>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The library as `parts.library` gives it.
|
||||||
|
pub fn families(v: &Value) -> Vec<Family> {
|
||||||
|
v.get("families")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|f| {
|
||||||
|
let id = f.get("family")?.as_str()?.to_string();
|
||||||
|
let sizes = f
|
||||||
|
.get("sizes")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|s| Some((s.get("size")?.as_str().map(str::to_string).or_else(|| s.get("size").map(Value::to_string))?, s.clone())))
|
||||||
|
.collect();
|
||||||
|
Some(Family {
|
||||||
|
name: f.get("name").and_then(Value::as_str).unwrap_or(&id).to_string(),
|
||||||
|
standard: f.get("standard").and_then(Value::as_str).unwrap_or("").to_string(),
|
||||||
|
sizes,
|
||||||
|
lengths: f.get("lengths").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_f64).collect()),
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Pt {
|
||||||
|
pub family: usize,
|
||||||
|
pub size: usize,
|
||||||
|
/// Index into the family's lengths (none: the default length).
|
||||||
|
pub length: Option<usize>,
|
||||||
|
pub name: String,
|
||||||
|
/// The (family, size, length) the picture shows (the texture is kept in egui's memory).
|
||||||
|
pub picture: Option<(usize, usize, Option<usize>)>,
|
||||||
|
/// The library, read once.
|
||||||
|
pub lib: Option<Vec<Family>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The dialog for Insert Part.
|
||||||
|
pub fn start(_app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||||
|
matches!(id, "FusionFastenersCommand" | "parts.insert").then(|| {
|
||||||
|
(
|
||||||
|
Kind::Part(Pt { family: 0, size: 0, length: None, name: String::new(), picture: None, lib: None }),
|
||||||
|
vec![SelInput::new("Hole (optional)", FACES | EDGES, false)],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn library(app: &mut SolveApp) -> Vec<Family> {
|
||||||
|
app.session.execute("parts.library", &json!({})).map(|v| families(&v)).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The insert command's parameters for the picks.
|
||||||
|
pub fn insert_params(lib: &[Family], k: &Pt, hole: Option<&Sel>) -> Result<Value, String> {
|
||||||
|
let f = lib.get(k.family).ok_or("the parts library is empty")?;
|
||||||
|
let (size, _) = f.sizes.get(k.size).ok_or("pick a size")?;
|
||||||
|
let mut p = json!({"family": f.id, "size": size});
|
||||||
|
if let Some(l) = k.length.and_then(|i| f.lengths.as_ref().and_then(|ls| ls.get(i))) {
|
||||||
|
p["length"] = json!(l);
|
||||||
|
}
|
||||||
|
match hole {
|
||||||
|
Some(Sel::Face { point, .. } | Sel::Edge { point, .. }) => p["at"] = json!([point.x, point.y, point.z]),
|
||||||
|
_ => {
|
||||||
|
p["point"] = json!([0.0, 0.0, 0.0]);
|
||||||
|
p["direction"] = json!([0.0, 0.0, 1.0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !k.name.trim().is_empty() {
|
||||||
|
p["name"] = json!(k.name.trim());
|
||||||
|
}
|
||||||
|
Ok(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A picture of the part alone.
|
||||||
|
fn picture(p: &Value) -> Option<egui::ColorImage> {
|
||||||
|
let mut s = Session::default();
|
||||||
|
let mut q = p.clone();
|
||||||
|
if let Value::Object(m) = &mut q {
|
||||||
|
m.remove("at");
|
||||||
|
m.insert("point".into(), json!([0.0, 0.0, 0.0]));
|
||||||
|
m.insert("direction".into(), json!([0.0, 0.0, 1.0]));
|
||||||
|
}
|
||||||
|
s.execute("parts.insert", &q).ok()?;
|
||||||
|
crate::home::thumbnail(&s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The dialog's rows.
|
||||||
|
pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Pt, inputs: &[SelInput]) -> bool {
|
||||||
|
let t = Tokens::get();
|
||||||
|
let lib = match &k.lib {
|
||||||
|
Some(l) => l.clone(),
|
||||||
|
None => {
|
||||||
|
let l = library(app);
|
||||||
|
k.lib = Some(l.clone());
|
||||||
|
l
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if lib.is_empty() {
|
||||||
|
row_label(ui, "");
|
||||||
|
ui.label(RichText::new("the parts library is not available").color(t.text_dim));
|
||||||
|
ui.end_row();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let names: Vec<String> =
|
||||||
|
lib.iter().map(|f| if f.standard.is_empty() { f.name.clone() } else { format!("{} ({})", f.name, f.standard) }).collect();
|
||||||
|
let refs: Vec<&str> = names.iter().map(String::as_str).collect();
|
||||||
|
row_label(ui, "Type");
|
||||||
|
let before = k.family;
|
||||||
|
combo(ui, "pt_family", &refs, &mut k.family);
|
||||||
|
ui.end_row();
|
||||||
|
if k.family != before {
|
||||||
|
k.size = 0;
|
||||||
|
k.length = None;
|
||||||
|
}
|
||||||
|
let Some(f) = lib.get(k.family) else { return false };
|
||||||
|
let sizes: Vec<&str> = f.sizes.iter().map(|s| s.0.as_str()).collect();
|
||||||
|
row_label(ui, "Size");
|
||||||
|
combo(ui, "pt_size", &sizes, &mut k.size);
|
||||||
|
ui.end_row();
|
||||||
|
if let Some(ls) = &f.lengths {
|
||||||
|
let mut labels = vec!["Default".to_string()];
|
||||||
|
labels.extend(ls.iter().map(|l| format!("{l} mm")));
|
||||||
|
let refs: Vec<&str> = labels.iter().map(String::as_str).collect();
|
||||||
|
let mut i = k.length.map_or(0, |x| x + 1);
|
||||||
|
row_label(ui, "Length");
|
||||||
|
combo(ui, "pt_len", &refs, &mut i);
|
||||||
|
k.length = i.checked_sub(1);
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
row_label(ui, "Name");
|
||||||
|
ui.add(egui::TextEdit::singleline(&mut k.name).hint_text("automatic"));
|
||||||
|
ui.end_row();
|
||||||
|
// The part, and its table values.
|
||||||
|
let key = (k.family, k.size, k.length);
|
||||||
|
let tex_id = egui::Id::new("sc_part_picture");
|
||||||
|
if k.picture != Some(key) {
|
||||||
|
let tex = insert_params(&lib, k, None)
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| picture(&p))
|
||||||
|
.map(|img| ui.ctx().load_texture("sc_part_picture", img, egui::TextureOptions::LINEAR));
|
||||||
|
ui.data_mut(|d| match tex {
|
||||||
|
Some(t) => {
|
||||||
|
d.insert_temp(tex_id, t);
|
||||||
|
}
|
||||||
|
None => d.remove::<egui::TextureHandle>(tex_id),
|
||||||
|
});
|
||||||
|
k.picture = Some(key);
|
||||||
|
}
|
||||||
|
row_label(ui, "");
|
||||||
|
match ui.data(|d| d.get_temp::<egui::TextureHandle>(tex_id)) {
|
||||||
|
Some(tx) => {
|
||||||
|
ui.add(egui::Image::new(&tx).fit_to_exact_size(vec2(180.0, 112.0)).corner_radius(4.0));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
ui.label(RichText::new("no picture").color(t.text_dim));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
if let Some((_, row)) = f.sizes.get(k.size)
|
||||||
|
&& let Some(o) = row.as_object()
|
||||||
|
{
|
||||||
|
for (key, v) in o.iter().filter(|(key, _)| key.as_str() != "size") {
|
||||||
|
row_label(ui, &key.replace('_', " "));
|
||||||
|
ui.label(RichText::new(v.as_f64().map(|x| format!("{x} mm")).unwrap_or_else(|| v.to_string())).color(t.text_dim));
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inputs.first().is_some_and(|i| i.items.is_empty()) {
|
||||||
|
row_label(ui, "");
|
||||||
|
ui.label(RichText::new("pick a hole to seat it, or OK places it at the origin").color(t.text_dim));
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The commands OK runs.
|
||||||
|
pub fn commands(app: &SolveApp, k: &Pt, inputs: &[SelInput]) -> Result<Vec<(String, Value)>, String> {
|
||||||
|
let lib = match &k.lib {
|
||||||
|
Some(l) => l.clone(),
|
||||||
|
None => app.session.scratch().execute("parts.library", &json!({})).map(|v| families(&v)).unwrap_or_default(),
|
||||||
|
};
|
||||||
|
let p = insert_params(&lib, k, inputs.first().and_then(|i| i.items.first()))?;
|
||||||
|
// The toolbar's command when the engine has it (same parameters).
|
||||||
|
let id = if solvecraft_engine::find_command("FusionFastenersCommand").is_some() { "FusionFastenersCommand" } else { "parts.insert" };
|
||||||
|
Ok(vec![(id.into(), p)])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use solvecraft_engine::geom::Vec3;
|
||||||
|
|
||||||
|
fn lib() -> Vec<Family> {
|
||||||
|
families(&json!({"families": [
|
||||||
|
{"family": "socket_head_cap_screw", "name": "Socket Head Cap Screw", "standard": "ISO 4762",
|
||||||
|
"sizes": [{"size": "M5", "head_diameter": 8.5}, {"size": "M6", "head_diameter": 10}], "lengths": [10, 16, 20]},
|
||||||
|
{"family": "bearing", "name": "Deep Groove Ball Bearing", "standard": "", "sizes": [{"size": "608", "bore": 8}], "lengths": null},
|
||||||
|
]}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn library_rows_parse() {
|
||||||
|
let l = lib();
|
||||||
|
assert_eq!(l.len(), 2);
|
||||||
|
assert_eq!(l[0].sizes[1].0, "M6");
|
||||||
|
assert_eq!(l[0].lengths.as_deref(), Some(&[10.0, 16.0, 20.0][..]));
|
||||||
|
assert!(l[1].lengths.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A hole pick seats the part (`at`); without one it goes to the origin along Z.
|
||||||
|
#[test]
|
||||||
|
fn insert_params_for_picks() {
|
||||||
|
let l = lib();
|
||||||
|
let k = Pt { family: 0, size: 1, length: Some(2), name: String::new(), picture: None, lib: None };
|
||||||
|
let hole = Sel::Face { body: "Body1".into(), index: 3, point: Vec3::new(5.0, 0.0, 10.0) };
|
||||||
|
let p = insert_params(&l, &k, Some(&hole)).unwrap();
|
||||||
|
assert_eq!(p, json!({"family": "socket_head_cap_screw", "size": "M6", "length": 20.0, "at": [5.0, 0.0, 10.0]}));
|
||||||
|
let k = Pt { family: 1, size: 0, length: None, name: "Front bearing".into(), picture: None, lib: None };
|
||||||
|
let p = insert_params(&l, &k, None).unwrap();
|
||||||
|
assert_eq!(p["point"], json!([0.0, 0.0, 0.0]));
|
||||||
|
assert_eq!(p["name"], "Front bearing");
|
||||||
|
assert!(p.get("length").is_none());
|
||||||
|
assert!(insert_params(&[], &k, None).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -856,6 +856,15 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce
|
|||||||
pen.arc(16.0, 11.5, 4.5, -1.57, 1.57, a);
|
pen.arc(16.0, 11.5, 4.5, -1.57, 1.57, a);
|
||||||
pen.line(&[(16.0, 7.0), (8.0, 7.0)], a);
|
pen.line(&[(16.0, 7.0), (8.0, 7.0)], a);
|
||||||
}
|
}
|
||||||
|
// Fastener: a socket head screw seen from the side.
|
||||||
|
"fastener" => {
|
||||||
|
pen.poly(&[(6.0, 3.0), (18.0, 3.0), (18.0, 9.0), (6.0, 9.0)], pen.fill, ink);
|
||||||
|
pen.line(&[(10.0, 3.0), (10.0, 6.0), (14.0, 6.0), (14.0, 3.0)], a);
|
||||||
|
pen.poly(&[(9.0, 9.0), (15.0, 9.0), (15.0, 21.0), (9.0, 21.0)], pen.fill.gamma_multiply(0.85), ink);
|
||||||
|
for y in [12.0, 15.0, 18.0] {
|
||||||
|
pen.line(&[(9.0, y), (15.0, y + 1.5)], ink);
|
||||||
|
}
|
||||||
|
}
|
||||||
"fold" => {
|
"fold" => {
|
||||||
pen.line(&[(2.0, 18.0), (12.0, 18.0)], ink);
|
pen.line(&[(2.0, 18.0), (12.0, 18.0)], ink);
|
||||||
pen.line(&[(12.0, 18.0), (19.0, 6.0)], a);
|
pen.line(&[(12.0, 18.0), (19.0, 6.0)], a);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub mod dialogs_appearance;
|
|||||||
pub mod dialogs_assembly;
|
pub mod dialogs_assembly;
|
||||||
pub mod dialogs_motion;
|
pub mod dialogs_motion;
|
||||||
pub mod dialogs_move;
|
pub mod dialogs_move;
|
||||||
|
pub mod dialogs_parts;
|
||||||
pub mod dialogs_plastic;
|
pub mod dialogs_plastic;
|
||||||
pub mod dialogs_sheet;
|
pub mod dialogs_sheet;
|
||||||
pub mod dim_view;
|
pub mod dim_view;
|
||||||
|
|||||||
Reference in new issue
Block a user