Canvas and Decal (FusionAddCanvasCommand, FusionAddEditDecalCommand): PNG/JPEG reference images stored in the design (base64), placed on an origin/construction plane, a sketch plane or a planar face; canvas.edit (position, width, angle, opacity, flip, visibility), canvas.calibrate (scale by a known distance), canvas.delete; drawn textured in the viewport; toolbar tools pick the file, then the plane or face
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
bc21959777
commit
00ddaa25f5
11 files changed
+506
-2
No files matched your search
@@ -21,6 +21,7 @@ mod menu_tests;
|
||||
pub mod palette;
|
||||
pub mod params_dialog;
|
||||
pub mod preview;
|
||||
pub mod ref_images;
|
||||
pub mod selection;
|
||||
pub mod sketch_dims;
|
||||
#[cfg(test)]
|
||||
@@ -282,6 +283,9 @@ impl SolveApp {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if !sketch_tools::pick_image(self, id) {
|
||||
return;
|
||||
}
|
||||
if id == "ChangeParameterCommand" {
|
||||
params_dialog::open(self);
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Canvases and decals: the design's reference images, drawn on their planes under the sketch
|
||||
//! overlays.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use egui::{Color32, Mesh, Shape, TextureHandle, TextureId, pos2};
|
||||
use solvecraft_engine::geom::Vec2;
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::viewport::Proj;
|
||||
|
||||
/// Largest texture side; bigger images are scaled down for display.
|
||||
const MAX_SIDE: u32 = 2048;
|
||||
/// Grid cells per side, so perspective views bend the image only slightly.
|
||||
const CELLS: usize = 8;
|
||||
|
||||
thread_local! {
|
||||
/// Textures by canvas id, with the length of the data they were made from.
|
||||
static TEX: RefCell<HashMap<u64, (usize, Option<TextureHandle>)>> = RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
fn texture(ctx: &egui::Context, id: u64, data: &str, bytes: impl FnOnce() -> Option<Vec<u8>>) -> Option<TextureId> {
|
||||
TEX.with(|t| {
|
||||
let mut t = t.borrow_mut();
|
||||
if let Some((len, h)) = t.get(&id)
|
||||
&& *len == data.len()
|
||||
{
|
||||
return h.as_ref().map(|h| h.id());
|
||||
}
|
||||
let img = bytes().and_then(|b| image::load_from_memory(&b).ok()).map(|i| {
|
||||
let i = if i.width() > MAX_SIDE || i.height() > MAX_SIDE { i.thumbnail(MAX_SIDE, MAX_SIDE) } else { i };
|
||||
let rgba = i.to_rgba8();
|
||||
egui::ColorImage::from_rgba_unmultiplied([rgba.width() as usize, rgba.height() as usize], rgba.as_raw())
|
||||
});
|
||||
let h = img.map(|img| ctx.load_texture(format!("sc_canvas_{id}"), img, egui::TextureOptions::LINEAR));
|
||||
let out = h.as_ref().map(|h| h.id());
|
||||
t.insert(id, (data.len(), h));
|
||||
out
|
||||
})
|
||||
}
|
||||
|
||||
pub fn show(app: &SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &Proj) {
|
||||
let doc = &app.session.doc;
|
||||
if doc.canvases.is_empty() {
|
||||
TEX.with(|t| t.borrow_mut().clear());
|
||||
return;
|
||||
}
|
||||
let (vals, _) = doc.param_values();
|
||||
for c in doc.canvases.iter().filter(|c| c.visible) {
|
||||
let Ok(plane) = doc.resolve_plane(&vals, &c.plane, 0) else { continue };
|
||||
let Some(tex) = texture(ui.ctx(), c.id, &c.data, || c.bytes()) else { continue };
|
||||
let [bl, br, _, tl] = c.corners();
|
||||
let (u, v) = (br - bl, tl - bl);
|
||||
let tint = Color32::from_white_alpha((c.opacity.clamp(0.0, 1.0) * 255.0) as u8);
|
||||
let mut mesh = Mesh::with_texture(tex);
|
||||
let mut ok = true;
|
||||
for j in 0..=CELLS {
|
||||
for i in 0..=CELLS {
|
||||
let (s, t) = (i as f64 / CELLS as f64, j as f64 / CELLS as f64);
|
||||
let p: Vec2 = bl + u * s + v * t;
|
||||
let Some(q) = proj.to_screen(plane.to_world(p)) else {
|
||||
ok = false;
|
||||
continue;
|
||||
};
|
||||
mesh.vertices.push(egui::epaint::Vertex { pos: q, uv: pos2(s as f32, 1.0 - t as f32), color: tint });
|
||||
}
|
||||
}
|
||||
// Behind the camera: skip rather than draw a torn image.
|
||||
if !ok {
|
||||
continue;
|
||||
}
|
||||
let w = (CELLS + 1) as u32;
|
||||
for j in 0..CELLS as u32 {
|
||||
for i in 0..CELLS as u32 {
|
||||
let a = j * w + i;
|
||||
mesh.add_triangle(a, a + 1, a + w + 1);
|
||||
mesh.add_triangle(a, a + w + 1, a + w);
|
||||
}
|
||||
}
|
||||
painter.add(Shape::mesh(mesh));
|
||||
}
|
||||
TEX.with(|t| t.borrow_mut().retain(|id, _| doc.canvases.iter().any(|c| c.id == *id)));
|
||||
}
|
||||
@@ -26,6 +26,19 @@ thread_local! {
|
||||
static PREV: Cell<Option<Vec2>> = const { Cell::new(None) };
|
||||
/// Curvature comb teeth (foot, tip) shown while the comb tool is active.
|
||||
static COMBS: RefCell<Vec<Vec<[Vec3; 2]>>> = const { RefCell::new(Vec::new()) };
|
||||
/// The image file a Canvas or Decal tool places.
|
||||
static IMAGE: RefCell<Option<String>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Canvas and Decal start by asking for the image; false when none was chosen.
|
||||
pub fn pick_image(app: &SolveApp, id: &str) -> bool {
|
||||
if !matches!(id, "FusionAddCanvasCommand" | "FusionAddEditDecalCommand") {
|
||||
return true;
|
||||
}
|
||||
let path = app.services.pick_open.as_ref().and_then(|f| f());
|
||||
let ok = path.is_some();
|
||||
IMAGE.with(|i| *i.borrow_mut() = path);
|
||||
ok
|
||||
}
|
||||
|
||||
fn v3(v: &Value) -> Option<Vec3> {
|
||||
@@ -175,7 +188,7 @@ enum Mode {
|
||||
fn mode(id: &str) -> Option<Mode> {
|
||||
Some(match id {
|
||||
"ProjectNewCmd" | "IntersectCmd" | "Include3DGeometry" | "FitCurvesToSectionCommand" | "SketchIsoparametricCurve" => Mode::ModelRef,
|
||||
"FusionCurvatureCombAnalysisCommand" => Mode::ModelRef,
|
||||
"FusionCurvatureCombAnalysisCommand" | "FusionAddCanvasCommand" | "FusionAddEditDecalCommand" => Mode::ModelRef,
|
||||
"TrimSketchCmd" | "ExtendSketchCmd" | "BreakSketchCmd" => Mode::CurveAt,
|
||||
"SketchMidpointLine" => Mode::Points(2),
|
||||
"ArcTangent" => Mode::Points(2),
|
||||
@@ -221,6 +234,8 @@ pub fn hint(id: &str) -> Option<String> {
|
||||
"FitCurvesToSectionCommand" => "Fit Curves to Mesh Section: click a body",
|
||||
"SketchIsoparametricCurve" => "Isoparametric Curve: click a point on a face (Shift: along)",
|
||||
"FusionCurvatureCombAnalysisCommand" => "Curvature comb: click sketch curves or model edges",
|
||||
"FusionAddCanvasCommand" => "Canvas: click a plane or a planar face where the image's centre goes",
|
||||
"FusionAddEditDecalCommand" => "Decal: click a planar face where the image's centre goes",
|
||||
"TrimSketchCmd" => "Trim: click the piece of a curve to remove",
|
||||
"ExtendSketchCmd" => "Extend: click a curve near the end to extend",
|
||||
"BreakSketchCmd" => "Break: click a curve where it should split",
|
||||
@@ -359,6 +374,37 @@ pub fn on_click(app: &mut SolveApp, tool: &mut Tool, proj: &Proj, pos: Pos2) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::ModelRef if matches!(cmd, "FusionAddCanvasCommand" | "FusionAddEditDecalCommand") => {
|
||||
let Some(path) = IMAGE.with(|i| i.borrow().clone()) else { return };
|
||||
let decal = cmd == "FusionAddEditDecalCommand";
|
||||
let target = hits.iter().find_map(|h| match h {
|
||||
Hit::Face { point, .. } => Some((json!({"face": [point.x, point.y, point.z]}), *point)),
|
||||
Hit::Plane { name, point } if !decal => Some((json!(name), *point)),
|
||||
_ => None,
|
||||
});
|
||||
// Empty space while sketching: the sketch plane.
|
||||
let target = target.or_else(|| {
|
||||
let st = app.session.model.state();
|
||||
let ss = st.sketch(app.session.active_sketch?)?;
|
||||
let (o, d) = proj.ray(pos);
|
||||
let w = ss.plane.intersect_ray(o, d)?;
|
||||
let pl = &ss.plane;
|
||||
Some((
|
||||
json!({"origin": [pl.origin.x, pl.origin.y, pl.origin.z], "x_dir": [pl.x.x, pl.x.y, pl.x.z], "y_dir": [pl.y.x, pl.y.y, pl.y.z]}),
|
||||
w,
|
||||
))
|
||||
});
|
||||
let Some((plane, at)) = target else { return };
|
||||
let p = if decal {
|
||||
json!({"path": path, "face": plane.get("face").cloned().unwrap_or(Value::Null)})
|
||||
} else {
|
||||
json!({"path": path, "plane": plane, "at": [at.x, at.y, at.z]})
|
||||
};
|
||||
if app.run(cmd, p).is_ok() {
|
||||
IMAGE.with(|i| *i.borrow_mut() = None);
|
||||
app.tool = None;
|
||||
}
|
||||
}
|
||||
Mode::ModelRef => {
|
||||
let r = hits.iter().find_map(|h| model_ref(app, h));
|
||||
let Some(r) = r else { return };
|
||||
|
||||
@@ -1070,6 +1070,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
} else {
|
||||
app.viewport.boxsel = None;
|
||||
}
|
||||
crate::ref_images::show(app, ui, &painter, &proj);
|
||||
overlays(app, &painter, &proj);
|
||||
crate::dim_view::show(app, ui, &painter, &proj);
|
||||
hover_highlight(app, &painter, &proj);
|
||||
|
||||
Reference in new issue
Block a user