Rename SolveCraft to SolidCraft everywhere, with compatibility shims (#389)

* WIP deep rename (not for push)

* WIP2

* WIP3

* WIP4 wxs fix

* rustfmt

* Rename the remaining identifiers in merged crash-report code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-12 17:17:01 -04:00
1 parent 27415e4dd7
commit 296683229d
464 files changed
+3408 -2956

No files matched your search

+3 -3
View File
@@ -4,8 +4,8 @@
use std::cell::RefCell;
use std::sync::Arc;
use solvecraft_engine::SurfaceAnalysis;
use solvecraft_engine::geom::Vec3;
use solidcraft_engine::SurfaceAnalysis;
use solidcraft_engine::geom::Vec3;
use crate::SolveApp;
use crate::gpu::AccessMap;
@@ -39,7 +39,7 @@ pub fn map(app: &SolveApp) -> Option<AccessMap> {
}
/// Rasterise the triangles seen along `dir` (orthographic), keeping the highest point per pixel.
fn build(meshes: &[&solvecraft_engine::geom::Mesh], dir: Vec3, version: u64) -> Option<AccessMap> {
fn build(meshes: &[&solidcraft_engine::geom::Mesh], dir: Vec3, version: u64) -> Option<AccessMap> {
let d = dir.normalized()?;
let u = d.cross(if d.x.abs() < 0.9 { Vec3::X } else { Vec3::Y }).normalized()?;
let v = d.cross(u);
+6 -6
View File
@@ -16,7 +16,7 @@ use std::sync::mpsc::Sender;
use egui::{Color32, FontId, Pos2, Shape, Stroke, pos2, vec2};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use solvecraft_engine::geom::{Vec2, Vec3};
use solidcraft_engine::geom::{Vec2, Vec3};
use crate::SolveApp;
use crate::control::ControlResponse;
@@ -132,7 +132,7 @@ pub struct AgentCursor {
/// Click ripples: where and when.
ripples: Vec<(Pos2, f64)>,
/// The view before a followed sketch (restored when it finishes).
saved_view: Option<solvecraft_engine::render::Camera>,
saved_view: Option<solidcraft_engine::render::Camera>,
}
impl AgentCursor {
@@ -149,7 +149,7 @@ const NOT_PLACES: [&str; 14] =
/// Where the pointer goes for a command: its toolbar button (or its panel), then each pick.
pub fn stops(app: &SolveApp, command: &str, params: &Value) -> Vec<Pos2> {
let mut out = Vec::new();
if let Some(spec) = solvecraft_engine::find_command(command) {
if let Some(spec) = solidcraft_engine::find_command(command) {
let button = crate::scenario::handle_at(&format!("toolbar:{}", spec.id))
.or_else(|| crate::scenario::handle_at(&format!("panel:{}:{}", spec.tab, spec.panel)));
out.extend(button);
@@ -167,7 +167,7 @@ pub fn stops(app: &SolveApp, command: &str, params: &Value) -> Vec<Pos2> {
/// The places a command's parameters pick, in the world: [x, y, z] points (faces, edges,
/// vertices, positions), active-sketch [x, y] points, and a new sketch's plane.
pub fn picks(app: &SolveApp, command: &str, params: &Value) -> Vec<Vec3> {
let id = solvecraft_engine::find_command(command).map_or(command, |c| c.id);
let id = solidcraft_engine::find_command(command).map_or(command, |c| c.id);
let mut out = Vec::new();
if id == "sketch.create"
&& let Some(n) = params.get("plane").and_then(Value::as_str)
@@ -308,7 +308,7 @@ fn finish(app: &mut SolveApp, ctx: &egui::Context) {
let mut params = job.params;
params["animate"] = json!(false);
let cmd = params.get("command").or(params.get("id")).and_then(Value::as_str).unwrap_or_default().to_string();
let id = solvecraft_engine::find_command(&cmd).map_or(cmd.clone(), |c| c.id.to_string());
let id = solidcraft_engine::find_command(&cmd).map_or(cmd.clone(), |c| c.id.to_string());
let follow = app.ui.agent_cursor.follow_camera;
if follow && id == "sketch.create" {
app.agent.saved_view = Some(app.cam);
@@ -360,7 +360,7 @@ pub fn paint(app: &SolveApp, ctx: &egui::Context) {
mod tests {
use super::*;
use crate::Services;
use solvecraft_engine::Session;
use solidcraft_engine::Session;
fn app() -> SolveApp {
let mut s = Session::default();
+18 -18
View File
@@ -14,8 +14,8 @@ use std::collections::BTreeSet;
use egui::{Color32, FontId, Pos2, Rect, RichText, Sense, Stroke, pos2, vec2};
use serde_json::{Value, json};
use solvecraft_engine::Sel;
use solvecraft_engine::doc::FeatureKind;
use solidcraft_engine::Sel;
use solidcraft_engine::doc::FeatureKind;
use crate::context_menu::{Item, Target};
use crate::theme::Tokens;
@@ -72,13 +72,13 @@ pub struct OccMove {
/// Exact assembly placement used for world-space movement and the pivot.
pub path: Vec<u64>,
/// Effective local placement at dialog opening, including its pre-existing pending move.
pub baseline: Option<solvecraft_engine::doc::Mat>,
pub baseline: Option<solidcraft_engine::doc::Mat>,
pub had_pending: bool,
/// Distances along X, Y, Z (mm) and turns about X, Y, Z (degrees) from where it was.
pub translate: [f64; 3],
pub angles: [f64; 3],
/// The occurrence's origin when the move began: what it turns about.
pub pivot: Option<solvecraft_engine::geom::Vec3>,
pub pivot: Option<solidcraft_engine::geom::Vec3>,
/// The values when the current triad drag began.
drag0: Option<([f64; 3], [f64; 3])>,
}
@@ -98,7 +98,7 @@ pub fn sketch_visible(app: &SolveApp, id: u64) -> bool {
if app.session.visibility.hidden_sketches.contains(&id) {
return false;
}
app.session.visibility.shown_sketches.contains(&id) || (app.ui.show_sketches && !solvecraft_engine::view::sketch_consumed(&app.session, id))
app.session.visibility.shown_sketches.contains(&id) || (app.ui.show_sketches && !solidcraft_engine::view::sketch_consumed(&app.session, id))
}
pub fn set_sketch_visible(app: &mut SolveApp, id: u64, on: bool) {
@@ -116,7 +116,7 @@ pub fn redefine_sketch(app: &mut SolveApp, id: u64) {
app.tree.redefine_draft = Some(crate::sketch_attachment_ui::Draft::new(app, id));
}
fn find_group(app: &SolveApp, id: u64) -> Option<&solvecraft_engine::doc::BrowserGroup> {
fn find_group(app: &SolveApp, id: u64) -> Option<&solidcraft_engine::doc::BrowserGroup> {
app.session.doc.browser_groups.iter().find(|g| g.id == id)
}
@@ -864,7 +864,7 @@ pub fn restore_view(app: &mut SolveApp, name: &str) {
}
let Some(v) = app.session.doc.named_views.iter().find(|v| v.name == name) else { return };
let j = json!({ "target": v.target, "yaw": v.yaw, "pitch": v.pitch, "distance": v.distance, "fov": v.fov });
if let Ok(cam) = serde_json::from_value::<solvecraft_engine::render::Camera>(j) {
if let Ok(cam) = serde_json::from_value::<solidcraft_engine::render::Camera>(j) {
app.animate_to(cam);
}
}
@@ -1072,7 +1072,7 @@ fn canvas_panel(app: &mut SolveApp, ctx: &egui::Context) {
/// otherwise it becomes the selection.
fn pick_from_browser(app: &mut SolveApp, x: Sel) {
let hit = match &x {
Sel::Plane { name } => crate::viewport::Hit::Plane { name: name.clone(), point: solvecraft_engine::geom::Vec3::ZERO },
Sel::Plane { name } => crate::viewport::Hit::Plane { name: name.clone(), point: solidcraft_engine::geom::Vec3::ZERO },
Sel::Axis { name } => crate::viewport::Hit::Axis { name: name.clone() },
_ => return,
};
@@ -1146,7 +1146,7 @@ fn entries(app: &SolveApp, comp: u64, folder: &str) -> Vec<Entry> {
.map(|f| {
let active = app.session.active_sketch == Some(f.id);
let lost = app.session.model.result(f.id).is_some_and(|result| result.error.is_some())
&& matches!(doc.sketch_face_attachment(f.id, &app.session.model), solvecraft_engine::doc::FaceAttachmentStatus::Lost { .. });
&& matches!(doc.sketch_face_attachment(f.id, &app.session.model), solidcraft_engine::doc::FaceAttachmentStatus::Lost { .. });
Entry {
key: f.id.to_string(),
row_key: format!("s:{}", f.id),
@@ -1190,9 +1190,9 @@ fn entries(app: &SolveApp, comp: u64, folder: &str) -> Vec<Entry> {
}
/// A folder's groups (with their items) and the items in no group, in browser order.
fn arrange(app: &SolveApp, comp: u64, folder: &str, all: Vec<Entry>) -> (Vec<(solvecraft_engine::doc::BrowserGroup, Vec<Entry>)>, Vec<Entry>) {
fn arrange(app: &SolveApp, comp: u64, folder: &str, all: Vec<Entry>) -> (Vec<(solidcraft_engine::doc::BrowserGroup, Vec<Entry>)>, Vec<Entry>) {
let doc = &app.session.doc;
let groups: Vec<solvecraft_engine::doc::BrowserGroup> =
let groups: Vec<solidcraft_engine::doc::BrowserGroup> =
doc.browser_groups.iter().filter(|g| g.component == comp && g.folder == folder).cloned().collect();
let mut rest: Vec<Option<Entry>> = all.into_iter().map(Some).collect();
let mut take = |key: &str| rest.iter_mut().find(|e| e.as_ref().is_some_and(|e| e.key == key)).and_then(Option::take);
@@ -1551,7 +1551,7 @@ fn occurrence_panel(app: &mut SolveApp, ui: &mut egui::Ui) {
m.pivot = if m.path.is_empty() {
occurrence_origin(app, m.occurrence)
} else {
app.session.occurrence_placement(&m.path).ok().map(|p| solvecraft_engine::doc::apply_point(&p.world, solvecraft_engine::geom::Vec3::ZERO))
app.session.occurrence_placement(&m.path).ok().map(|p| solidcraft_engine::doc::apply_point(&p.world, solidcraft_engine::geom::Vec3::ZERO))
};
}
let name = app.session.doc.occurrences.iter().find(|o| o.id == m.occurrence).map(|o| o.name.clone()).unwrap_or_default();
@@ -1629,7 +1629,7 @@ pub(crate) fn revert_occurrence_dialog(app: &mut SolveApp, m: &OccMove) {
/// The pending move: turned about X, then Y, then Z through the starting origin, then moved.
pub fn apply_occurrence_move(app: &mut SolveApp, m: &OccMove) {
use crate::gizmo;
use solvecraft_engine::geom::Vec3;
use solidcraft_engine::geom::Vec3;
let a = m.angles.map(f64::to_radians);
let r = gizmo::mul(&gizmo::mul(&gizmo::rotation(Vec3::Z, a[2]), &gizmo::rotation(Vec3::Y, a[1])), &gizmo::rotation(Vec3::X, a[0]));
let (axis, angle) = gizmo::axis_angle(&r);
@@ -1650,7 +1650,7 @@ fn triad(app: &mut SolveApp, ui: &mut egui::Ui, m: &mut OccMove) {
use crate::gizmo::{self, Change};
let (Some(rect), Some(pivot)) = (app.viewport.rect, m.pivot) else { return };
let proj = crate::viewport::projection(app, rect);
let center = pivot + solvecraft_engine::geom::Vec3::new(m.translate[0], m.translate[1], m.translate[2]);
let center = pivot + solidcraft_engine::geom::Vec3::new(m.translate[0], m.translate[1], m.translate[2]);
let layer = egui::LayerId::new(egui::Order::Middle, egui::Id::new("sc_occ_triad_layer"));
let mut gui = ui.new_child(egui::UiBuilder::new().layer_id(layer).max_rect(rect));
let painter = gui.painter().with_clip_rect(rect);
@@ -1677,8 +1677,8 @@ fn triad(app: &mut SolveApp, ui: &mut egui::Ui, m: &mut OccMove) {
/// Dimensions of finished sketches whose dimensions are shown (Show Dimensions), drawn over the
/// viewport like the sketch being edited draws its own (`crate::dim_view`), but not editable.
pub fn dims_overlay(app: &SolveApp, ctx: &egui::Context) {
use solvecraft_engine::geom::Vec2;
use solvecraft_engine::sketch::{DimFrame, chain_centres, default_text, dim_frame, dim_layout};
use solidcraft_engine::geom::Vec2;
use solidcraft_engine::sketch::{DimFrame, chain_centres, default_text, dim_frame, dim_layout};
let Some(rect) = app.viewport.rect else { return };
if app.ui.shown_dims.is_empty() {
return;
@@ -1815,9 +1815,9 @@ fn capture_prompt(app: &mut SolveApp, ctx: &egui::Context) {
}
/// Where an occurrence's origin is now (with its pending move).
fn occurrence_origin(app: &SolveApp, occurrence: u64) -> Option<solvecraft_engine::geom::Vec3> {
fn occurrence_origin(app: &SolveApp, occurrence: u64) -> Option<solidcraft_engine::geom::Vec3> {
let o = app.session.doc.occurrences.iter().find(|o| o.id == occurrence)?;
let parent = app.session.doc.component_transform(o.parent);
let own = app.session.pending_moves.get(&occurrence).copied().unwrap_or(o.transform);
Some(solvecraft_engine::doc::apply_point(&solvecraft_engine::doc::mat_mul(&parent, &own), solvecraft_engine::geom::Vec3::ZERO))
Some(solidcraft_engine::doc::apply_point(&solidcraft_engine::doc::mat_mul(&parent, &own), solidcraft_engine::geom::Vec3::ZERO))
}
+28 -28
View File
@@ -6,9 +6,9 @@
//! moves on, Esc cancels.
use egui::{Color32, Pos2, Stroke, vec2};
use solvecraft_engine::Sel;
use solvecraft_engine::doc::expr::Kind as ValueKind;
use solvecraft_engine::geom::{Mesh, Vec3};
use solidcraft_engine::Sel;
use solidcraft_engine::doc::expr::Kind as ValueKind;
use solidcraft_engine::geom::{Mesh, Vec3};
use crate::SolveApp;
use crate::dialogs::{Dialog, Kind};
@@ -36,9 +36,9 @@ pub fn anchor(app: &SolveApp, d: &Dialog) -> Option<(Vec3, Option<Vec3>)> {
})
.map(|p| p.region.clone())
.collect();
let regions = solvecraft_engine::sketch::merge_regions(&regions);
let regions = solidcraft_engine::sketch::merge_regions(&regions);
let points: Vec<_> = regions.iter().flat_map(|r| r.outer.polyline(1e-2)).collect();
points.iter().fold(solvecraft_engine::geom::Vec2::default(), |sum, p| sum + *p) * (1.0 / points.len().max(1) as f64)
points.iter().fold(solidcraft_engine::geom::Vec2::default(), |sum, p| sum + *p) * (1.0 / points.len().max(1) as f64)
} else {
p.region.interior_point()
};
@@ -54,8 +54,8 @@ pub fn anchor(app: &SolveApp, d: &Dialog) -> Option<(Vec3, Option<Vec3>)> {
}
Sel::Body { name } => (st.body(name)?.mesh().bounds().center(), None),
Sel::Plane { name } => {
let pl = solvecraft_engine::geom::Plane::named(name).or_else(|| {
solvecraft_engine::view::construction_planes(&app.session).into_iter().find(|(_, n, _)| n == name).map(|(_, _, pl)| pl)
let pl = solidcraft_engine::geom::Plane::named(name).or_else(|| {
solidcraft_engine::view::construction_planes(&app.session).into_iter().find(|(_, n, _)| n == name).map(|(_, _, pl)| pl)
})?;
(pl.origin, Some(pl.normal()))
}
@@ -64,7 +64,7 @@ pub fn anchor(app: &SolveApp, d: &Dialog) -> Option<(Vec3, Option<Vec3>)> {
Some(match &d.kind {
Kind::Extrude { start, .. } => {
let n = normal?;
let plane = solvecraft_engine::geom::Plane::from_normal(at, n)?;
let plane = solidcraft_engine::geom::Plane::from_normal(at, n)?;
let from = reference_height(app, d, "From Object", &plane, at).unwrap_or(0.0);
let offset = if start.trim().is_empty() { 0.0 } else { app.session.doc.eval(start, ValueKind::Length).ok()? };
(at + n * (from + offset), Some(n))
@@ -79,23 +79,23 @@ pub fn anchor(app: &SolveApp, d: &Dialog) -> Option<(Vec3, Option<Vec3>)> {
})
}
fn profile_plane(app: &SolveApp, sketch: u64) -> Option<solvecraft_engine::geom::Plane> {
fn profile_plane(app: &SolveApp, sketch: u64) -> Option<solidcraft_engine::geom::Plane> {
let component = app.session.doc.feature(sketch)?.component;
let state = app.session.model.state();
let plane = state.sketch(sketch)?.plane;
let frame = crate::dialogs::feature_frame(&app.session, component);
solvecraft_engine::geom::Plane::new(
solvecraft_engine::doc::apply_point(&frame, plane.origin),
solvecraft_engine::doc::apply_vector(&frame, plane.x),
solvecraft_engine::doc::apply_vector(&frame, plane.y),
solidcraft_engine::geom::Plane::new(
solidcraft_engine::doc::apply_point(&frame, plane.origin),
solidcraft_engine::doc::apply_vector(&frame, plane.x),
solidcraft_engine::doc::apply_vector(&frame, plane.y),
)
}
fn reference_height(app: &SolveApp, d: &Dialog, label: &str, plane: &solvecraft_engine::geom::Plane, centre: Vec3) -> Option<f64> {
fn reference_height(app: &SolveApp, d: &Dialog, label: &str, plane: &solidcraft_engine::geom::Plane, centre: Vec3) -> Option<f64> {
let point = crate::dialogs::object_point(&d.inputs, label)?;
let p = point.as_array()?;
let p = Vec3::new(p.first()?.as_f64()?, p.get(1)?.as_f64()?, p.get(2)?.as_f64()?);
Some(solvecraft_engine::doc::object_height(&app.session.world_state(), plane, centre, p))
Some(solidcraft_engine::doc::object_height(&app.session.world_state(), plane, centre, p))
}
/// The outward direction between the two faces at an edge (a fillet's radius arrow points the
@@ -195,7 +195,7 @@ fn offset_plane_quad(proj: &Proj, c: Vec3, n: Vec3, h: f64) -> Option<Vec<Pos2>>
fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj, d: &mut Dialog, base: Vec3, bs: Pos2, dir: Option<Vec3>) {
let t = Tokens::get();
let to_span = dir.and_then(|n| {
let plane = solvecraft_engine::geom::Plane::from_normal(base, n)?;
let plane = solidcraft_engine::geom::Plane::from_normal(base, n)?;
reference_height(app, d, "To Object", &plane, base)
});
let all_span = if matches!(d.kind, Kind::Extrude { all: true, .. }) {
@@ -203,7 +203,7 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
let state = app.session.world_state();
let plane = match d.inputs.first()?.items.first()? {
Sel::Profile { sketch, .. } => profile_plane(app, *sketch)?,
_ => solvecraft_engine::geom::Plane::from_normal(base, n)?,
_ => solidcraft_engine::geom::Plane::from_normal(base, n)?,
};
let targets: Vec<String> = d
.extra
@@ -211,7 +211,7 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
.and_then(serde_json::Value::as_array)
.map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_owned)).collect())
.unwrap_or_default();
solvecraft_engine::doc::through_all_distance(&state, &plane, &targets).ok()
solidcraft_engine::doc::through_all_distance(&state, &plane, &targets).ok()
})
} else {
None
@@ -446,7 +446,7 @@ pub(crate) fn revolve_axis(app: &SolveApp, d: &Dialog) -> Option<(Vec3, Vec3)> {
"Y" => Vec3::Y,
_ => Vec3::Z,
};
return Some((solvecraft_engine::doc::apply_point(&frame, Vec3::ZERO), solvecraft_engine::doc::apply_vector(&frame, dir)));
return Some((solidcraft_engine::doc::apply_point(&frame, Vec3::ZERO), solidcraft_engine::doc::apply_vector(&frame, dir)));
}
if let Sel::Axis { name } = selected
&& matches!(name.as_str(), "x" | "y")
@@ -479,7 +479,7 @@ fn draw_revolve(
focus: bool,
) {
let to_mode = d.inputs.iter().any(|input| input.label == "To Object");
let extent = revolve_display_extent(app, d, axis).unwrap_or(solvecraft_engine::doc::RevolveExtent {
let extent = revolve_display_extent(app, d, axis).unwrap_or(solidcraft_engine::doc::RevolveExtent {
start: 0.0,
sweep: f64::NAN,
rotation_axis: axis.1,
@@ -495,8 +495,8 @@ fn draw_revolve(
let e1 = (base - centre) / radius;
let e2 = extent.rotation_axis.cross(e1);
let handed = match revolve_convention(d) {
solvecraft_engine::doc::RevolveConvention::LegacyAxisStart => -direction.dot(extent.rotation_axis),
solvecraft_engine::doc::RevolveConvention::SketchNormal => 1.0,
solidcraft_engine::doc::RevolveConvention::LegacyAxisStart => -direction.dot(extent.rotation_axis),
solidcraft_engine::doc::RevolveConvention::SketchNormal => 1.0,
};
let at = |angle: f64| centre + (e1 * angle.cos() + e2 * angle.sin()) * radius;
let points: Vec<_> = (0..=48).filter_map(|i| proj.to_screen(at(extent.display_start + extent.sweep * i as f64 / 48.0))).collect();
@@ -611,14 +611,14 @@ fn draw_revolve(
});
}
fn revolve_convention(d: &Dialog) -> solvecraft_engine::doc::RevolveConvention {
fn revolve_convention(d: &Dialog) -> solidcraft_engine::doc::RevolveConvention {
d.extra
.get("convention")
.and_then(|value| serde_json::from_value(value.clone()).ok())
.unwrap_or(solvecraft_engine::doc::RevolveConvention::SketchNormal)
.unwrap_or(solidcraft_engine::doc::RevolveConvention::SketchNormal)
}
fn revolve_display_extent(app: &SolveApp, d: &Dialog, axis: (Vec3, Vec3)) -> Option<solvecraft_engine::doc::RevolveExtent> {
fn revolve_display_extent(app: &SolveApp, d: &Dialog, axis: (Vec3, Vec3)) -> Option<solidcraft_engine::doc::RevolveExtent> {
let Sel::Profile { sketch, .. } = d.inputs.first()?.items.first()? else { return None };
let state = app.session.world_state();
let ss = state.sketch(*sketch)?;
@@ -632,7 +632,7 @@ fn revolve_display_extent(app: &SolveApp, d: &Dialog, axis: (Vec3, Vec3)) -> Opt
_ => None,
})
.collect();
let regions = solvecraft_engine::sketch::merge_regions(&regions);
let regions = solidcraft_engine::sketch::merge_regions(&regions);
let plane = profile_plane(app, *sketch)?;
let to_mode = d.inputs.iter().any(|input| input.label == "To Object");
let to = if to_mode {
@@ -649,7 +649,7 @@ fn revolve_display_extent(app: &SolveApp, d: &Dialog, axis: (Vec3, Vec3)) -> Opt
let other = d.extra.get("angle2").map(|value| app.session.doc.eval(value.as_str().unwrap_or_default(), ValueKind::Angle)).transpose().ok()?;
(angle, other)
};
solvecraft_engine::doc::revolve_extent_with_convention(
solidcraft_engine::doc::revolve_extent_with_convention(
&state,
&plane,
&regions,
@@ -788,7 +788,7 @@ mod tests {
let mut worst = f32::NEG_INFINITY;
for n in [Vec3::X, Vec3::Y, Vec3::Z] {
for dir in crate::viewport::orbit_directions() {
let cam = solvecraft_engine::render::Camera::default().looking_from(dir);
let cam = solidcraft_engine::render::Camera::default().looking_from(dir);
let proj = Proj { cam, vp: cam.view_proj(4.0 / 3.0, 100.0), rect };
let h = cam.half_height() * 0.25;
let (Some(q), Some(c), Some(side)) =
+1 -1
View File
@@ -2,7 +2,7 @@
use crate::SolveApp;
use egui::{RichText, Ui};
use serde_json::{Value, json};
use solvecraft_engine::doc::config::{Column, ConfigRule, RuleCondition, RuleEffect, RuleOperator};
use solidcraft_engine::doc::config::{Column, ConfigRule, RuleCondition, RuleEffect, RuleOperator};
#[derive(Clone, Debug)]
pub struct Editor {
+6 -6
View File
@@ -14,7 +14,7 @@
use egui::{Align2, Color32, FontId, Pos2, Rect, Sense, Stroke, Vec2, pos2, vec2};
use serde::Serialize;
use serde_json::{Value, json};
use solvecraft_engine::Sel;
use solidcraft_engine::Sel;
use crate::theme::Tokens;
use crate::{SolveApp, icons};
@@ -123,7 +123,7 @@ impl Item {
/// A command entry: label, icon, shortcut and enablement come from the registry.
fn cmd(app: &SolveApp, id: &str, label: &str) -> Item {
let info = solvecraft_engine::find_command(id).map(|c| c.info(&app.session));
let info = solidcraft_engine::find_command(id).map(|c| c.info(&app.session));
let shortcut = match id {
"edit.undo" => "Ctrl+Z".to_string(),
"edit.redo" => "Ctrl+Y".to_string(),
@@ -493,7 +493,7 @@ fn sketch_items(app: &SolveApp, id: u64) -> Vec<Item> {
/// An assembly joint: edit, drive, suppress, delete.
fn joint_items(app: &SolveApp, id: u64) -> Vec<Item> {
let Some(j) = app.session.doc.assembly.joints.iter().find(|j| j.id == id) else { return Vec::new() };
let rigid = j.kind == solvecraft_engine::doc::joints::JointKind::Rigid;
let rigid = j.kind == solidcraft_engine::doc::joints::JointKind::Rigid;
vec![
act("ui.editJoint", "Edit Joint", "joint").with(json!({ "joint": id })),
act("ui.editJoint", "Edit Joint Limits", "").with(json!({ "joint": id })).on(!rigid),
@@ -609,7 +609,7 @@ fn component_items(app: &SolveApp, id: u64) -> Vec<Item> {
/// Sketch entity items: construction, fix, delete, dimension and the constraints the selection
/// can take.
fn sketch_entity_items(app: &SolveApp) -> Vec<Item> {
use solvecraft_engine::sketch::CurveKind;
use solidcraft_engine::sketch::CurveKind;
let Some(sid) = app.session.active_sketch else { return Vec::new() };
let st = app.session.world_state();
let Some(ss) = st.sketch(sid) else { return Vec::new() };
@@ -1024,7 +1024,7 @@ pub fn delete(app: &mut SolveApp) {
/// visibility (bodies, faces' and edges' bodies, sketches, construction and origin planes and
/// axes, components' bodies, canvases). When some are shown and some hidden, all are hidden.
pub fn toggle_visibility(app: &mut SolveApp) {
use solvecraft_engine::doc::FeatureKind;
use solidcraft_engine::doc::FeatureKind;
let paths: Vec<Vec<u64>> =
app.session.selection.iter().filter_map(|s| if let Sel::Occurrence { path } = s { Some(path.clone()) } else { None }).collect();
if !paths.is_empty() {
@@ -1686,7 +1686,7 @@ mod tests {
#[test]
fn disabled_radial_menu_does_not_capture_right_button_hold() {
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
let ctx = egui::Context::default();
let point = pos2(600.0, 400.0);
app.viewport.mouse = Some(point);
+11 -11
View File
@@ -37,7 +37,7 @@
use std::sync::mpsc::Sender;
use serde_json::{Value, json};
use solvecraft_engine::render::{StandardView, render_png};
use solidcraft_engine::render::{StandardView, render_png};
use crate::SolveApp;
@@ -180,7 +180,7 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
};
}
}
match (serde_json::from_value::<crate::UiState>(cur), serde_json::from_value::<solvecraft_engine::Visibility>(vis)) {
match (serde_json::from_value::<crate::UiState>(cur), serde_json::from_value::<solidcraft_engine::Visibility>(vis)) {
(Ok(u), Ok(v)) => {
app.ui = u;
app.session.visibility = v;
@@ -349,13 +349,13 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
let proj = crate::viewport::projection(app, rect);
let num = |v: &Value, i: usize| v.get(i).and_then(Value::as_f64).unwrap_or(0.0);
let world = if let Some(w) = p.get("world") {
Some(solvecraft_engine::geom::Vec3::new(num(w, 0), num(w, 1), num(w, 2)))
Some(solidcraft_engine::geom::Vec3::new(num(w, 0), num(w, 1), num(w, 2)))
} else if let Some(s) = p.get("sketch") {
let st = app.session.world_state();
app.session
.active_sketch
.and_then(|id| st.sketch(id))
.map(|ss| ss.plane.to_world(solvecraft_engine::geom::Vec2::new(num(s, 0), num(s, 1))))
.map(|ss| ss.plane.to_world(solidcraft_engine::geom::Vec2::new(num(s, 0), num(s, 1))))
} else if let Some(n) = p.get("plane").and_then(Value::as_str) {
crate::viewport::origin_planes(app).into_iter().find(|(name, _, _)| *name == n).map(|(_, _, q)| (q[0] + q[1] + q[2] + q[3]) / 4.0)
} else if let Some(n) = p.get("axis").and_then(Value::as_str) {
@@ -365,7 +365,7 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
} else if let Some(n) = p.get("gizmo").and_then(Value::as_str) {
// A handle of the move triad ("x", "xy", "rz", "ball"), moved `by` [x,y,z] mm or
// turned `turn` degrees (rings).
let by = p.get("by").map(|b| solvecraft_engine::geom::Vec3::new(num(b, 0), num(b, 1), num(b, 2))).unwrap_or_default();
let by = p.get("by").map(|b| solidcraft_engine::geom::Vec3::new(num(b, 0), num(b, 1), num(b, 2))).unwrap_or_default();
crate::gizmo::handle_point(n, by, p.get("turn").and_then(Value::as_f64).unwrap_or(0.0))
} else {
None
@@ -386,7 +386,7 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
.take(1000)
.map(|v| {
let c = |i: usize| v.get(i).and_then(Value::as_f64).unwrap_or(0.0);
proj.to_screen(solvecraft_engine::geom::Vec3::new(c(0), c(1), c(2))).map_or(Value::Null, |q| json!([q.x, q.y]))
proj.to_screen(solidcraft_engine::geom::Vec3::new(c(0), c(1), c(2))).map_or(Value::Null, |q| json!([q.x, q.y]))
})
.collect();
ok(json!(pts))
@@ -406,7 +406,7 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
.take(1000)
.map(|v| {
let c = |i: usize| v.get(i).and_then(Value::as_f64).unwrap_or(0.0);
let q = solvecraft_engine::geom::Vec2::new(c(0), c(1));
let q = solidcraft_engine::geom::Vec2::new(c(0), c(1));
proj.to_screen(ss.plane.to_world(q)).map_or(Value::Null, |p| json!([p.x, p.y]))
})
.collect();
@@ -527,7 +527,7 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
let h = f("height").unwrap_or(800.0).clamp(16.0, 8192.0) as usize;
// Cut by the section the viewport shows (an open Section Analysis dialog's preview,
// a sketch's Slice), not only the committed one.
let scene = solvecraft_engine::view::scene_with_section(&app.session, &app.cam, crate::viewport::section_plane(app));
let scene = solidcraft_engine::view::scene_with_section(&app.session, &app.cam, crate::viewport::section_plane(app));
let Some(png) = render_png(&scene, &app.cam, w, h) else { return err("render failed") };
match s("path") {
Some(path) => match std::fs::write(path, &png) {
@@ -561,7 +561,7 @@ pub fn save_screenshot(image: &egui::ColorImage, path: Option<&str>) -> Value {
buf.extend_from_slice(&c.to_array());
}
let Some(img) = image::RgbaImage::from_raw(w as u32, h as u32, buf) else { return json!({"ok": false, "error": "bad image"}) };
let path = path.map(str::to_string).unwrap_or_else(|| std::env::temp_dir().join("solvecraft-shot.png").to_string_lossy().to_string());
let path = path.map(str::to_string).unwrap_or_else(|| std::env::temp_dir().join("solidcraft-shot.png").to_string_lossy().to_string());
match img.save(&path) {
Ok(()) => json!({"ok": true, "result": {"path": path, "width": w, "height": h}}),
Err(e) => json!({"ok": false, "error": e.to_string()}),
@@ -571,7 +571,7 @@ pub fn save_screenshot(image: &egui::ColorImage, path: Option<&str>) -> Value {
#[cfg(test)]
mod tests {
use serde_json::json;
use solvecraft_engine::{Sel, Session};
use solidcraft_engine::{Sel, Session};
use super::*;
use crate::Services;
@@ -592,7 +592,7 @@ mod tests {
let mut s = Session::default();
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
let mut app = SolveApp::new(s, Services::default());
let dir = std::env::temp_dir().join(format!("solvecraft-render-section-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("solidcraft-render-section-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let whole = render(&mut app, &dir.join("whole.png"));
app.session.selection = vec![Sel::Plane { name: "XZ".into() }];
+7 -7
View File
@@ -1,8 +1,8 @@
//! The Crash Reports window (Help > Crash Reports…): the reports `solvecraft_engine::crash`
//! The Crash Reports window (Help > Crash Reports…): the reports `solidcraft_engine::crash`
//! keeps on this computer, each with a reviewed issue draft. It opens by itself after a crash.
use egui::{RichText, vec2};
use solvecraft_engine::crash::{self, Summary};
use solidcraft_engine::crash::{self, Summary};
use crate::SolveApp;
use crate::theme::Tokens;
@@ -153,12 +153,12 @@ mod tests {
use serde_json::json;
fn folder(tag: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("solvecraft-crash-ui-{tag}-{}", std::process::id()));
let d = std::env::temp_dir().join(format!("solidcraft-crash-ui-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
let id = "20261012-162341-bb60791ed6692f1d";
std::fs::create_dir_all(d.join(id)).unwrap();
let report = json!({
"format": "solvecraft-crash/1", "id": id, "kind": "unclean_exit", "count": 2,
"format": "solidcraft-crash/1", "id": id, "kind": "unclean_exit", "count": 2,
"first_seen": "2026-10-12T16:23:41Z", "message": "SolidCraft ended without a clean exit while running `solid.fillet`",
"location": "unclean exit in solid.fillet", "thread": "(unknown)", "backtrace": [],
"command": {"command": "solid.fillet", "params": {"radius": 2}},
@@ -176,7 +176,7 @@ mod tests {
let d = folder("draft");
let mut h = crate::scenario::Harness::new();
h.app.crashes.folder = Some(d.clone());
let fresh = solvecraft_engine::crash::list(&d);
let fresh = solidcraft_engine::crash::list(&d);
assert_eq!(fresh.len(), 1);
super::announce(&mut h.app, &fresh);
assert!(h.app.crashes.open, "a real crash opens the window");
@@ -201,7 +201,7 @@ mod tests {
h.frames(2);
h.step(&json!({"widget": "Delete all"})).unwrap();
h.frames(2);
assert!(solvecraft_engine::crash::list(&d).is_empty());
assert!(solidcraft_engine::crash::list(&d).is_empty());
let _ = std::fs::remove_dir_all(&d);
}
@@ -221,7 +221,7 @@ mod tests {
#[test]
fn recovered_kernel_errors_do_not_interrupt() {
let mut h = crate::scenario::Harness::new();
let quiet = solvecraft_engine::crash::Summary {
let quiet = solidcraft_engine::crash::Summary {
id: "x".into(),
kind: "kernel_panic_recovered".into(),
time: String::new(),
+2 -2
View File
@@ -18,8 +18,8 @@ pub struct Pending {
/// What Delete acts on now: the selection (bodies not locked), plus the components picked in
/// the browser (their occurrences).
pub fn selection_params(app: &SolveApp) -> Value {
let items: Vec<&solvecraft_engine::Sel> =
app.session.selection.iter().filter(|s| !matches!(s, solvecraft_engine::Sel::Body { name } if app.ui.locked_bodies.contains(name))).collect();
let items: Vec<&solidcraft_engine::Sel> =
app.session.selection.iter().filter(|s| !matches!(s, solidcraft_engine::Sel::Body { name } if app.ui.locked_bodies.contains(name))).collect();
let occurrences: Vec<u64> = app.tree.picked_components.iter().filter_map(|c| app.session.doc.occurrence_of(*c).map(|o| o.id)).collect();
json!({ "items": items, "occurrences": occurrences, "canvases": app.tree.picked_canvases })
}
+41 -41
View File
@@ -6,11 +6,11 @@ use std::hash::{Hash, Hasher};
use egui::{Color32, RichText, Stroke, vec2};
use serde_json::{Value, json};
use solvecraft_engine::Sel;
use solvecraft_engine::Session;
use solvecraft_engine::doc::expr::Kind as ValueKind;
use solvecraft_engine::doc::{AxisRef, Direction, FeatureKind, FilletStyle, HoleKind, Operation, PatternKind, PlaneRef, ProfileSel};
use solvecraft_engine::geom::Vec3;
use solidcraft_engine::Sel;
use solidcraft_engine::Session;
use solidcraft_engine::doc::expr::Kind as ValueKind;
use solidcraft_engine::doc::{AxisRef, Direction, FeatureKind, FilletStyle, HoleKind, Operation, PatternKind, PlaneRef, ProfileSel};
use solidcraft_engine::geom::Vec3;
use crate::SolveApp;
use crate::selection::{self, AXES, Accept, BODIES, CURVES, EDGES, FACES, PLANAR_FACES, PLANES, PROFILES, SelInput};
@@ -936,7 +936,7 @@ fn feature_of_face(s: &Session, body: &str, index: usize, p: Vec3) -> Option<u64
}) {
return hit;
}
let on_model = |st: &solvecraft_engine::doc::ModelState| {
let on_model = |st: &solidcraft_engine::doc::ModelState| {
st.bodies.iter().any(|b| {
let m = b.mesh();
let bb = m.bounds();
@@ -1173,13 +1173,13 @@ fn update_fill_cells(app: &SolveApp, d: &mut Dialog) {
/// The cells a set of tool bodies makes: each region inside some of them and outside the rest,
/// as a point inside it and which tools hold it. Found by sampling a grid over the tools.
pub(crate) fn fill_cells(st: &solvecraft_engine::doc::ModelState, tools: &[String]) -> Vec<(Vec3, String)> {
pub(crate) fn fill_cells(st: &solidcraft_engine::doc::ModelState, tools: &[String]) -> Vec<(Vec3, String)> {
let bodies: Vec<_> = tools.iter().filter_map(|n| st.body(n).map(|b| (n.clone(), b.mesh()))).take(16).collect();
if bodies.is_empty() {
return Vec::new();
}
let indexes: Vec<_> = bodies.iter().map(|(_, m)| m.inside_index()).collect();
let bb = bodies.iter().fold(solvecraft_engine::geom::Aabb3::EMPTY, |b, (_, m)| b.union(&m.bounds()));
let bb = bodies.iter().fold(solidcraft_engine::geom::Aabb3::EMPTY, |b, (_, m)| b.union(&m.bounds()));
let n = 20;
let at = |i: usize, lo: f64, hi: f64| lo + (hi - lo) * (i as f64 + 0.5) / n as f64;
// Signature (which tools hold the point) → the points with it.
@@ -1280,7 +1280,7 @@ fn auto_operation(app: &SolveApp, d: &mut Dialog) {
return;
}
let Some(op) =
dialog_commands(app, d).ok().and_then(|c| c.into_iter().next()).and_then(|(_, p)| solvecraft_engine::auto_operation(&app.session, &p))
dialog_commands(app, d).ok().and_then(|c| c.into_iter().next()).and_then(|(_, p)| solidcraft_engine::auto_operation(&app.session, &p))
else {
return;
};
@@ -1816,7 +1816,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
}
}
Kind::Material { index } => {
let names: Vec<&str> = solvecraft_engine::doc::MATERIALS.iter().map(|(n, _)| *n).collect();
let names: Vec<&str> = solidcraft_engine::doc::MATERIALS.iter().map(|(n, _)| *n).collect();
row_label(ui, "Material");
combo(ui, "mat", &names, index);
ui.end_row();
@@ -2372,7 +2372,7 @@ fn run_dialog(app: &mut SolveApp, d: &Dialog) -> Result<(), String> {
}
/// Placement used to edit a definition in the selected authoring occurrence.
pub(crate) fn feature_frame(s: &Session, component: u64) -> solvecraft_engine::doc::Mat {
pub(crate) fn feature_frame(s: &Session, component: u64) -> solidcraft_engine::doc::Mat {
if component == s.active_component { s.active_component_frame() } else { s.placed_document().component_transform(component) }
}
@@ -2427,7 +2427,7 @@ pub fn preview_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value
/// A wrapped Emboss edit keeps its profile guide in the saved author occurrence.
/// An invalid saved context must not draw the guide at a different occurrence.
pub(crate) fn emboss_profile_plane(app: &SolveApp, sketch: u64) -> Result<Option<solvecraft_engine::geom::Plane>, String> {
pub(crate) fn emboss_profile_plane(app: &SolveApp, sketch: u64) -> Result<Option<solidcraft_engine::geom::Plane>, String> {
let Some(dialog) = app.dialog.as_ref().filter(|d| matches!(d.kind, Kind::Emboss { .. })) else { return Ok(None) };
let Some((feature, _)) = dialog.editing else { return Ok(None) };
let Some(feature) = app.session.doc.feature(feature) else { return Err("the edited feature is missing".into()) };
@@ -2441,10 +2441,10 @@ pub(crate) fn emboss_profile_plane(app: &SolveApp, sketch: u64) -> Result<Option
let owner = app.session.doc.feature(sketch.feature).map(|feature| feature.component).unwrap_or(0);
let plane = sketch.plane;
let frame = face.sketch_frame(&app.session.doc, feature.component, owner, body).map_err(|error| error.to_string())?;
solvecraft_engine::geom::Plane::new(
solvecraft_engine::doc::apply_point(&frame, plane.origin),
solvecraft_engine::doc::apply_vector(&frame, plane.x),
solvecraft_engine::doc::apply_vector(&frame, plane.y),
solidcraft_engine::geom::Plane::new(
solidcraft_engine::doc::apply_point(&frame, plane.origin),
solidcraft_engine::doc::apply_vector(&frame, plane.x),
solidcraft_engine::doc::apply_vector(&frame, plane.y),
)
.map(Some)
.ok_or_else(|| "the Emboss author frame is degenerate".into())
@@ -2837,7 +2837,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
}
Kind::Material { index } => {
need(0, "bodies")?;
let name = solvecraft_engine::doc::MATERIALS.get(*index).map(|(n, _)| *n).unwrap_or("Default");
let name = solidcraft_engine::doc::MATERIALS.get(*index).map(|(n, _)| *n).unwrap_or("Default");
("material.assign", json!({"bodies": body_names(0), "material": name}))
}
Kind::Section { offset, flip } => {
@@ -2849,7 +2849,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
json!({"origin": pt(*point), "normal": pt(n)})
}
Some(Sel::Plane { name }) => {
let pl = solvecraft_engine::view::construction_planes(s)
let pl = solidcraft_engine::view::construction_planes(s)
.into_iter()
.find(|(_, n, _)| n == name)
.map(|(_, _, pl)| pl)
@@ -3042,14 +3042,14 @@ pub fn axis_of(app: &SolveApp, sel: &Sel) -> Option<(Vec3, Vec3)> {
"Y" => Some((Vec3::ZERO, Vec3::Y)),
"Z" => Some((Vec3::ZERO, Vec3::Z)),
// A construction axis.
n => solvecraft_engine::view::construction_axes(&app.session).into_iter().find(|(_, an, _, _)| an == n).map(|(_, _, o, d)| (o, d)),
n => solidcraft_engine::view::construction_axes(&app.session).into_iter().find(|(_, an, _, _)| an == n).map(|(_, _, o, d)| (o, d)),
},
Sel::SketchCurve { id } => {
let st = app.session.world_state();
let sid = curves_sketch(app, std::slice::from_ref(id), None)?;
let ss = st.sketch(sid)?;
let c = ss.sketch.curves.get(ss.sketch.curve_index(id)?)?;
let solvecraft_engine::sketch::CurveKind::Line { a, b } = c.kind else { return None };
let solidcraft_engine::sketch::CurveKind::Line { a, b } = c.kind else { return None };
let (pa, pb) = (ss.plane.to_world(ss.sketch.point(a)?), ss.plane.to_world(ss.sketch.point(b)?));
Some((pa, (pb - pa).normalized()?))
}
@@ -3073,21 +3073,21 @@ pub fn axis_of(app: &SolveApp, sel: &Sel) -> Option<(Vec3, Vec3)> {
}
/// Which profiles of a sketch a profile selection means.
pub(crate) fn profile_indices(ss: &solvecraft_engine::doc::SolvedSketch, sel: &ProfileSel) -> Vec<usize> {
pub(crate) fn profile_indices(ss: &solidcraft_engine::doc::SolvedSketch, sel: &ProfileSel) -> Vec<usize> {
let ps = &ss.profiles;
match sel {
ProfileSel::All => (0..ps.len()).collect(),
ProfileSel::Indices { indices } => indices.iter().copied().filter(|i| *i < ps.len()).collect(),
ProfileSel::OffsetBoundaries { .. } | ProfileSel::WholeBoundaries { .. } => {
solvecraft_engine::doc::profile_selection_indices(ss, sel).unwrap_or_default()
solidcraft_engine::doc::profile_selection_indices(ss, sel).unwrap_or_default()
}
ProfileSel::Curves { loops } => loops
.iter()
.filter_map(|l| {
solvecraft_engine::doc::profile_selection_indices(ss, &ProfileSel::Curves { loops: vec![l.clone()] }).ok()?.first().copied()
solidcraft_engine::doc::profile_selection_indices(ss, &ProfileSel::Curves { loops: vec![l.clone()] }).ok()?.first().copied()
})
.collect(),
ProfileSel::Points { points } => points.iter().filter_map(|q| solvecraft_engine::doc::sketch::profile_at_point(ps, *q)).collect(),
ProfileSel::Points { points } => points.iter().filter_map(|q| solidcraft_engine::doc::sketch::profile_at_point(ps, *q)).collect(),
}
}
@@ -3115,13 +3115,13 @@ impl Drop for Refill {
/// A point stored in a feature, as it shows on world body `body`: features keep their picks in
/// the frame of the body they were on, so each body's placement takes the point to the world
/// (a moved component, a pasted instance). Other points are the world already.
fn stored_on(frames: &Option<Vec<(String, solvecraft_engine::doc::Mat)>>, body: &str, p: Vec3) -> Vec3 {
frames.as_ref().and_then(|f| f.iter().find(|(n, _)| n == body)).map(|(_, m)| solvecraft_engine::doc::apply_point(m, p)).unwrap_or(p)
fn stored_on(frames: &Option<Vec<(String, solidcraft_engine::doc::Mat)>>, body: &str, p: Vec3) -> Vec3 {
frames.as_ref().and_then(|f| f.iter().find(|(n, _)| n == body)).map(|(_, m)| solidcraft_engine::doc::apply_point(m, p)).unwrap_or(p)
}
/// Body placements while filling a dialog from a feature (when anything is placed).
fn stored_frames(s: &Session) -> Option<Vec<(String, solvecraft_engine::doc::Mat)>> {
if !REFILL.with(std::cell::Cell::get) || !solvecraft_engine::frames::placed(s) {
fn stored_frames(s: &Session) -> Option<Vec<(String, solidcraft_engine::doc::Mat)>> {
if !REFILL.with(std::cell::Cell::get) || !solidcraft_engine::frames::placed(s) {
return None;
}
let st = s.model.state();
@@ -3165,14 +3165,14 @@ pub(crate) fn face_sel(s: &Session, p0: Vec3) -> Option<Sel> {
best.filter(|(d, _)| *d < 1e-3 + 1e-6 * p0.len()).map(|x| x.1)
}
fn emboss_face_sel(s: &Session, component: u64, face: &solvecraft_engine::doc::EmbossFace, persistent: &str) -> Option<Sel> {
fn emboss_face_sel(s: &Session, component: u64, face: &solidcraft_engine::doc::EmbossFace, persistent: &str) -> Option<Sel> {
let source = s.model.state();
let body = source.body(&face.body)?;
let index = solvecraft_engine::doc::naming::face_names(body).iter().position(|name| name == persistent)?;
let index = solidcraft_engine::doc::naming::face_names(body).iter().position(|name| name == persistent)?;
let owner = s.doc.model_body_component(body);
let world_name = s.doc.world_body_name(&source, &face.body, owner, &face.path);
let author = solvecraft_engine::doc::EmbossFace::component_frame(&s.doc, component, &face.author_path).ok()?;
let point = solvecraft_engine::doc::apply_point(&author, face.at);
let author = solidcraft_engine::doc::EmbossFace::component_frame(&s.doc, component, &face.author_path).ok()?;
let point = solidcraft_engine::doc::apply_point(&author, face.at);
s.world_state().body(&world_name)?;
Some(Sel::Face { body: world_name, index, point })
}
@@ -3207,7 +3207,7 @@ fn pt3(v: Vec3) -> Value {
}
/// A point of expressions: numbers as numbers, expressions as strings.
fn pt_expr(p: &solvecraft_engine::doc::PointExpr) -> Value {
fn pt_expr(p: &solidcraft_engine::doc::PointExpr) -> Value {
Value::Array(p.iter().map(|e| e.parse::<f64>().ok().filter(|x| x.is_finite()).map_or_else(|| json!(e), |x| json!(x))).collect())
}
@@ -3246,7 +3246,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
if let Some(point) = point {
set_object(&mut d.inputs, &mut d.active, label, true);
if let Some(input) = d.inputs.iter_mut().find(|i| i.label == label) {
input.items = vec![Sel::Vertex { body: String::new(), point: solvecraft_engine::doc::apply_point(&frame, point) }];
input.items = vec![Sel::Vertex { body: String::new(), point: solidcraft_engine::doc::apply_point(&frame, point) }];
}
}
}
@@ -3264,8 +3264,8 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
let frame = feature_frame(s, f.component);
let stored_axis = match axis {
AxisRef::Line { origin, dir } => Some(json!({
"origin": pt(solvecraft_engine::doc::apply_point(&frame, *origin)),
"dir": pt(solvecraft_engine::doc::apply_vector(&frame, *dir))
"origin": pt(solidcraft_engine::doc::apply_point(&frame, *origin)),
"dir": pt(solidcraft_engine::doc::apply_vector(&frame, *dir))
})),
_ => None,
};
@@ -3280,7 +3280,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
if let Some(point) = to {
set_to_object(&mut d.inputs, &mut d.active, true);
if let Some(input) = d.inputs.iter_mut().find(|i| i.label == TO_OBJECT) {
input.items = vec![Sel::Vertex { body: String::new(), point: solvecraft_engine::doc::apply_point(&frame, *point) }];
input.items = vec![Sel::Vertex { body: String::new(), point: solidcraft_engine::doc::apply_point(&frame, *point) }];
}
}
let items = st.sketch(*sketch).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
@@ -3320,7 +3320,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
}
}
// A fillet that reduces a radius too large for its edges.
if let FeatureKind::Fillet { fit: solvecraft_engine::doc::FilletFit::Reduce, .. } = &f.kind {
if let FeatureKind::Fillet { fit: solidcraft_engine::doc::FilletFit::Reduce, .. } = &f.kind {
d.extra.insert("fit".into(), json!("reduce"));
}
// A chord-length or variable fillet.
@@ -3631,8 +3631,8 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
FeatureKind::ConstructionPlane { plane: PlaneRef::AlongPath { path, t } } => {
let mut d = start("construct.plane.along_path")?;
let (item, sketch) = match path.as_ref() {
solvecraft_engine::doc::construct::GeoRef::SketchCurve { sketch, curve } => (Sel::SketchCurve { id: curve.clone() }, Some(*sketch)),
solvecraft_engine::doc::construct::GeoRef::Edge { at, name } => {
solidcraft_engine::doc::construct::GeoRef::SketchCurve { sketch, curve } => (Sel::SketchCurve { id: curve.clone() }, Some(*sketch)),
solidcraft_engine::doc::construct::GeoRef::Edge { at, name } => {
// Preserve the saved named reference until a replacement is picked.
d.extra.insert("path".into(), json!({"type":"edge","at":pt3(*at),"name":name}));
(Sel::Edge { body: String::new(), index: 0, point: *at }, None)
@@ -3798,7 +3798,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
#[cfg(test)]
mod param_refusal_tests {
use super::*;
use solvecraft_engine::Session;
use solidcraft_engine::Session;
#[test]
fn a_refused_parameter_change_shows_why() {
+6 -6
View File
@@ -5,7 +5,7 @@
use egui::{Color32, RichText, Sense, Stroke, vec2};
use serde_json::{Value, json};
use solvecraft_engine::Sel;
use solidcraft_engine::Sel;
use crate::SolveApp;
use crate::dialogs::{Kind, combo, row_label};
@@ -118,12 +118,12 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
let faces = app.session.selection.iter().any(|s| matches!(s, Sel::Face { .. }))
&& !app.session.selection.iter().any(|s| matches!(s, Sel::Body { .. }));
let state = app.session.world_state();
let body_look = |body: &solvecraft_engine::doc::ModelBody| {
let body_look = |body: &solidcraft_engine::doc::ModelBody| {
app.session.doc.model_body_look(body).or_else(|| {
(body.body.paint().is_some() || body.imported.is_some() || body.body.color().is_some()).then(|| {
let color =
app.session.doc.body_color(body).map(|rgb| rgb.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)).unwrap_or([196, 199, 204]);
solvecraft_engine::doc::appearance::Look::custom(color, app.session.doc.model_body_opacity(body))
solidcraft_engine::doc::appearance::Look::custom(color, app.session.doc.model_body_opacity(body))
})
})
};
@@ -169,7 +169,7 @@ fn apply_params(targets: &[Sel], color: [u8; 3], opacity: f64, look: Option<&str
if !faces.is_empty() {
p["faces"] = json!(faces);
// An engine without face looks: their bodies.
if bodies.is_empty() && solvecraft_engine::find_command("appearance.list").is_none() {
if bodies.is_empty() && solidcraft_engine::find_command("appearance.list").is_none() {
let mut owners: Vec<String> =
targets.iter().filter_map(|x| if let Sel::Face { body, .. } = x { Some(body.clone()) } else { None }).collect();
owners.dedup();
@@ -483,8 +483,8 @@ fn target_names(targets: &[Sel]) -> String {
#[cfg(test)]
mod tests {
use solvecraft_engine::Session;
use solvecraft_engine::geom::Vec3;
use solidcraft_engine::Session;
use solidcraft_engine::geom::Vec3;
use super::*;
use crate::Services;
+13 -13
View File
@@ -7,11 +7,11 @@
use egui::{Color32, Pos2, RichText, Stroke, vec2};
use serde_json::{Value, json};
use solvecraft_engine::Sel;
use solvecraft_engine::Session;
use solvecraft_engine::doc::expr::Kind as ValueKind;
use solvecraft_engine::doc::joints::JointKind;
use solvecraft_engine::geom::{Aabb3, Mesh, Vec3};
use solidcraft_engine::Sel;
use solidcraft_engine::Session;
use solidcraft_engine::doc::expr::Kind as ValueKind;
use solidcraft_engine::doc::joints::JointKind;
use solidcraft_engine::geom::{Aabb3, Mesh, Vec3};
use crate::SolveApp;
use crate::dialogs::{Dialog, Kind, combo, field, row_label};
@@ -335,7 +335,7 @@ pub fn snap_of(s: &Session, sel: &Sel) -> Option<SnapPt> {
match f.plane_normal {
Some(n) => (body, SnapPt { at: f.centroid, z: n, kind: SnapKind::FaceCenter, param: json!({"face": pt(*point)}) }),
None => {
let c = solvecraft_engine::kernel::cylinder_face_at(&b.body, *point)?;
let c = solidcraft_engine::kernel::cylinder_face_at(&b.body, *point)?;
let sv = (*point - c.axis_point).dot(c.axis);
let (at, dir) = if (sv - c.start).abs() <= (c.end - sv).abs() { (c.start, -1.0) } else { (c.end, 1.0) };
(
@@ -614,7 +614,7 @@ fn interference(app: &mut SolveApp, picked: &[Sel]) -> (Value, Vec<Mesh>) {
let mut meshes = Vec::new();
for hit in r["interferences"].as_array().into_iter().flatten() {
let (Some(a), Some(b)) = (hit["a"].as_str().and_then(|n| st.body(n)), hit["b"].as_str().and_then(|n| st.body(n))) else { continue };
if let Ok(Some(o)) = solvecraft_engine::kernel::boolean(&a.body, &b.body, solvecraft_engine::kernel::BoolOp::Intersect)
if let Ok(Some(o)) = solidcraft_engine::kernel::boolean(&a.body, &b.body, solidcraft_engine::kernel::BoolOp::Intersect)
&& let Ok(m) = o.tessellate((o.size() * 2e-3).max(1e-3))
{
meshes.push(m);
@@ -816,7 +816,7 @@ pub fn world_preview(s: &Session, cmds: &[(String, Value)], colors: Colors) -> O
// Any feature, when components are placed away from their frames: the preview must show
// the world the viewport shows.
let moves = cmds.iter().all(|(c, _)| MOVES.contains(&c.as_str()));
if cmds.is_empty() || !(moves || solvecraft_engine::frames::placed(s)) {
if cmds.is_empty() || !(moves || solidcraft_engine::frames::placed(s)) {
return None;
}
let preview = match s.preview_visible_world(cmds) {
@@ -1086,12 +1086,12 @@ mod tests;
thread_local! {
/// Named joint origins in the world, for the design state they were found in.
static ORIGINS: std::cell::RefCell<Option<(String, Vec<(String, solvecraft_engine::doc::Mat)>)>> = const { std::cell::RefCell::new(None) };
static ORIGINS: std::cell::RefCell<Option<(String, Vec<(String, solidcraft_engine::doc::Mat)>)>> = const { std::cell::RefCell::new(None) };
}
/// Named joint origins in the world: on their occurrence's component, so they move with it
/// (joint drives, free moves not captured yet, Capture Position).
pub fn origins_world(s: &Session) -> Vec<(String, solvecraft_engine::doc::Mat)> {
pub fn origins_world(s: &Session) -> Vec<(String, solidcraft_engine::doc::Mat)> {
if s.doc.assembly.origins.is_empty() {
return Vec::new();
}
@@ -1099,7 +1099,7 @@ pub fn origins_world(s: &Session) -> Vec<(String, solvecraft_engine::doc::Mat)>
if let Some(v) = ORIGINS.with(|o| o.borrow().as_ref().filter(|(k, _)| *k == key).map(|(_, v)| v.clone())) {
return v;
}
let doc: std::borrow::Cow<solvecraft_engine::doc::Document> = if s.pending_moves.is_empty() {
let doc: std::borrow::Cow<solidcraft_engine::doc::Document> = if s.pending_moves.is_empty() {
std::borrow::Cow::Borrowed(&*s.doc)
} else {
let mut d = (*s.doc).clone();
@@ -1121,10 +1121,10 @@ pub fn origins(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
let t = Tokens::get();
let len = app.cam.half_height() * 2.0 * 0.04;
for (_, m) in origins_world(&app.session) {
let at = solvecraft_engine::doc::apply_point(&m, Vec3::ZERO);
let at = solidcraft_engine::doc::apply_point(&m, Vec3::ZERO);
let Some(c) = proj.to_screen(at) else { continue };
for (axis, col) in [(Vec3::X, t.axis_x), (Vec3::Y, t.axis_y), (Vec3::Z, t.axis_z)] {
if let Some(tip) = proj.to_screen(at + solvecraft_engine::doc::apply_vector(&m, axis) * len) {
if let Some(tip) = proj.to_screen(at + solidcraft_engine::doc::apply_vector(&m, axis) * len) {
painter.line_segment([c, tip], Stroke::new(2.0, col));
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
use serde_json::json;
use solvecraft_engine::Session;
use solidcraft_engine::Session;
use super::*;
use crate::Services;
@@ -273,7 +273,7 @@ fn joint_origins_follow_their_component() {
let at = |app: &SolveApp| {
let v = origins_world(&app.session);
let (_, m) = v.iter().find(|(n, _)| n == "Top of B").unwrap();
solvecraft_engine::doc::apply_point(m, Vec3::ZERO)
solidcraft_engine::doc::apply_point(m, Vec3::ZERO)
};
assert!(at(&app).dist(Vec3::new(40.0, 10.0, 10.0)) < 1e-6, "{:?}", at(&app));
let b = occ(&app, "B");
+18 -18
View File
@@ -7,9 +7,9 @@ use std::collections::BTreeMap;
use egui::{Color32, Pos2, RichText, Sense, Stroke, pos2, vec2};
use serde_json::{Value, json};
use solvecraft_engine::Session;
use solvecraft_engine::doc::Mat;
use solvecraft_engine::doc::joints::JointKind;
use solidcraft_engine::Session;
use solidcraft_engine::doc::Mat;
use solidcraft_engine::doc::joints::JointKind;
use crate::SolveApp;
use crate::dialogs::{Dialog, Kind, combo, field, row_label};
@@ -251,10 +251,10 @@ pub fn table(v: &Value) -> Table {
}
/// The unit a value of this kind is shown in.
fn unit_of(k: solvecraft_engine::doc::expr::Kind) -> Option<&'static str> {
fn unit_of(k: solidcraft_engine::doc::expr::Kind) -> Option<&'static str> {
match k {
solvecraft_engine::doc::expr::Kind::Length => Some("mm"),
solvecraft_engine::doc::expr::Kind::Angle => Some("deg"),
solidcraft_engine::doc::expr::Kind::Length => Some("mm"),
solidcraft_engine::doc::expr::Kind::Angle => Some("deg"),
_ => None,
}
}
@@ -604,7 +604,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel
&& let Some(unit) = app.session.doc.param(n).map(|parameter| parameter.unit.clone()).or_else(|| {
kinds.iter().find(|x| x.0 == n).and_then(|x| {
unit_of(x.2).map(|unit| {
if x.2 == solvecraft_engine::doc::expr::Kind::Length { app.session.doc.units.clone() } else { unit.to_string() }
if x.2 == solidcraft_engine::doc::expr::Kind::Length { app.session.doc.units.clone() } else { unit.to_string() }
})
})
})
@@ -675,10 +675,10 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel
let (kind, _) = column_parts(c);
let appearance = matches!(kind, "appearance" | "component_appearance" | "face_appearance");
let shown = if appearance {
solvecraft_engine::doc::config::appearance_cell(val)
solidcraft_engine::doc::config::appearance_cell(val)
.ok()
.flatten()
.filter(|look| solvecraft_engine::doc::appearance::library(&look.name).as_ref() == Some(look))
.filter(|look| solidcraft_engine::doc::appearance::library(&look.name).as_ref() == Some(look))
.map(|look| look.name)
.unwrap_or_else(|| val.clone())
} else {
@@ -702,14 +702,14 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel
ui.close();
}
if kind == "material" {
for (name, _) in solvecraft_engine::doc::MATERIALS.iter().filter(|(n, _)| *n != "Default") {
for (name, _) in solidcraft_engine::doc::MATERIALS.iter().filter(|(n, _)| *n != "Default") {
if ui.button(*name).clicked() {
choice = Some(name.to_string());
ui.close();
}
}
} else {
for (name, _, _) in solvecraft_engine::doc::appearance::LIBRARY {
for (name, _, _) in solidcraft_engine::doc::appearance::LIBRARY {
if ui.button(name).clicked() {
choice = Some(name.to_string());
ui.close();
@@ -785,7 +785,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel
}
}
for selected in &app.session.selection {
if let solvecraft_engine::Sel::Face { body, point, .. } = selected {
if let solidcraft_engine::Sel::Face { body, point, .. } = selected {
options
.push((format!("Selected face on {body} (shared appearance)"), json!({"face_appearance":{"body":body,"point":point}})));
}
@@ -930,7 +930,7 @@ pub fn commands(app: &SolveApp, k: &Mo, inputs: &[SelInput]) -> Result<Vec<(Stri
Mo::ContactSet { name, .. } => {
let mut occs: Vec<u64> = Vec::new();
for x in inputs.first().map(|i| i.items.as_slice()).unwrap_or(&[]) {
if let solvecraft_engine::Sel::Body { name } = x
if let solidcraft_engine::Sel::Body { name } = x
&& let Some(b) = app.session.world_state().body(name)
{
let comp = app.session.doc.model_body_component(b);
@@ -974,9 +974,9 @@ mod tests {
#[test]
fn shown_poses_are_put_back() {
let mut app = SolveApp::new(Session::default(), crate::Services::default());
app.session.pending_moves.insert(9, solvecraft_engine::doc::IDENTITY);
app.session.pending_moves.insert(9, solidcraft_engine::doc::IDENTITY);
let before = app.session.pending_moves.clone();
let mut m = solvecraft_engine::doc::IDENTITY;
let mut m = solidcraft_engine::doc::IDENTITY;
m[3] = [5.0, 0.0, 0.0, 1.0];
let shown: BTreeMap<u64, Mat> = [(3, m)].into_iter().collect();
for (kind, applied, kept) in [("study", true, false), ("explode", false, false), ("explode", true, true)] {
@@ -1061,7 +1061,7 @@ mod tests {
select_rule_parameter(&mut h, "length");
h.step(&json!({"widget":"Save Rule"})).unwrap();
assert!(
matches!(&h.app.session.doc.configs.rules[0].condition,solvecraft_engine::doc::config::RuleCondition::All{conditions} if matches!(&conditions[0],solvecraft_engine::doc::config::RuleCondition::Parameter{..}))
matches!(&h.app.session.doc.configs.rules[0].condition,solidcraft_engine::doc::config::RuleCondition::All{conditions} if matches!(&conditions[0],solidcraft_engine::doc::config::RuleCondition::Parameter{..}))
);
h.run(&json!([
{"call":"engine.execute","params":{"command":"config.rule","params":{"rule":{"id":"last","name":"Last drive","condition":{"type":"always"},"effect":{"type":"drive_parameter","parameter":"length","expression":"20 mm"}}}}},
@@ -1179,8 +1179,8 @@ mod tests {
assert!(h.widget("side:outer").is_some());
assert!(h.widget("side:inner").is_some());
h.run(&json!([{"widget":"side:outer"},{"widget":"Library"},{"widget":"ABS - Red"},{"widget":"Apply"}])).unwrap();
let solvecraft_engine::doc::config::Column::FaceAppearance(target) = &h.app.session.doc.configs.columns[0] else { panic!() };
assert!(matches!(&target.certificate, Some(solvecraft_engine::doc::config::FaceCertificate::ExtrudeSide { curve, .. }) if curve == "outer"));
let solidcraft_engine::doc::config::Column::FaceAppearance(target) = &h.app.session.doc.configs.columns[0] else { panic!() };
assert!(matches!(&target.certificate, Some(solidcraft_engine::doc::config::FaceCertificate::ExtrudeSide { curve, .. }) if curve == "outer"));
let state = h.app.session.model.state();
let body = state.body("Ring").unwrap();
let indices = h.app.session.doc.configuration_face_indices(body, target).unwrap();
+3 -3
View File
@@ -3,9 +3,9 @@
//! moves until a value is typed or the triad is dragged.
use serde_json::{Value, json};
use solvecraft_engine::Sel;
use solvecraft_engine::doc::expr::Kind as ValueKind;
use solvecraft_engine::geom::Vec3;
use solidcraft_engine::Sel;
use solidcraft_engine::doc::expr::Kind as ValueKind;
use solidcraft_engine::geom::Vec3;
use crate::SolveApp;
use crate::dialogs::{Dialog, Kind, combo, field, row_label};
+3 -3
View File
@@ -5,7 +5,7 @@
use egui::{RichText, vec2};
use serde_json::{Value, json};
use solvecraft_engine::{Sel, Session};
use solidcraft_engine::{Sel, Session};
use crate::SolveApp;
use crate::dialogs::{Kind, combo, row_label};
@@ -207,14 +207,14 @@ pub fn commands(app: &SolveApp, k: &Pt, inputs: &[SelInput]) -> Result<Vec<(Stri
};
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("parts.fastener").is_some() { "parts.fastener" } else { "parts.insert" };
let id = if solidcraft_engine::find_command("parts.fastener").is_some() { "parts.fastener" } else { "parts.insert" };
Ok(vec![(id.into(), p)])
}
#[cfg(test)]
mod tests {
use super::*;
use solvecraft_engine::geom::Vec3;
use solidcraft_engine::geom::Vec3;
fn lib() -> Vec<Family> {
families(&json!({"families": [
+5 -5
View File
@@ -2,11 +2,11 @@
//! Plastic Rules and Assign Plastic Rule. Values left empty take the body's plastic rule.
use serde_json::{Map, Value, json};
use solvecraft_engine::Sel;
use solvecraft_engine::doc::FeatureKind;
use solvecraft_engine::doc::expr::Kind as ValueKind;
use solvecraft_engine::doc::plastic::PlasticRule;
use solvecraft_engine::geom::Vec3;
use solidcraft_engine::Sel;
use solidcraft_engine::doc::FeatureKind;
use solidcraft_engine::doc::expr::Kind as ValueKind;
use solidcraft_engine::doc::plastic::PlasticRule;
use solidcraft_engine::geom::Vec3;
use crate::SolveApp;
use crate::dialogs::{Kind, combo, face_sel, field, row_label};
+1 -1
View File
@@ -1,5 +1,5 @@
use serde_json::json;
use solvecraft_engine::Session;
use solidcraft_engine::Session;
use super::*;
use crate::Services;
+5 -5
View File
@@ -5,11 +5,11 @@
use egui::{Align2, Color32, FontId, Pos2, Rect, RichText, Stroke, pos2, vec2};
use serde_json::{Map, Value, json};
use solvecraft_engine::Sel;
use solvecraft_engine::doc::expr::Kind as ValueKind;
use solvecraft_engine::doc::sheet::SheetRule;
use solvecraft_engine::doc::{FeatureKind, ProfileSel};
use solvecraft_engine::geom::Vec3;
use solidcraft_engine::Sel;
use solidcraft_engine::doc::expr::Kind as ValueKind;
use solidcraft_engine::doc::sheet::SheetRule;
use solidcraft_engine::doc::{FeatureKind, ProfileSel};
use solidcraft_engine::geom::Vec3;
use crate::SolveApp;
use crate::dialogs::{Dialog, Kind, combo, edge_sel, face_sel, field, profile_indices, row_label};
+2 -2
View File
@@ -1,5 +1,5 @@
use serde_json::json;
use solvecraft_engine::Session;
use solidcraft_engine::Session;
use super::*;
use crate::Services;
@@ -295,7 +295,7 @@ fn bend_table_picker_writes_the_current_body() {
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
app.run("sheet.flange", json!({"edges": [[50, 0, 2.5]], "height": 20})).unwrap();
let body = app.session.model.state().sheets[0].body.clone();
let path = std::env::temp_dir().join(format!("solvecraft-bend-table-{}.csv", std::process::id()));
let path = std::env::temp_dir().join(format!("solidcraft-bend-table-{}.csv", std::process::id()));
let output = path.to_string_lossy().into_owned();
let chosen = output.clone();
app.services.pick_save = Some(Box::new(move |_, extensions| {
+4 -4
View File
@@ -2,10 +2,10 @@
//! Trim and Split Face. Each has a live preview and Edit Feature.
use serde_json::{Map, Value, json};
use solvecraft_engine::Sel;
use solvecraft_engine::doc::expr::Kind as ValueKind;
use solvecraft_engine::doc::{FaceAt, FeatureKind, PlaneRef};
use solvecraft_engine::geom::Vec3;
use solidcraft_engine::Sel;
use solidcraft_engine::doc::expr::Kind as ValueKind;
use solidcraft_engine::doc::{FaceAt, FeatureKind, PlaneRef};
use solidcraft_engine::geom::Vec3;
use crate::SolveApp;
use crate::dialogs::{Kind, OP_LABELS, OPS, combo, field, op_index, row_label};
+15 -15
View File
@@ -2,7 +2,7 @@
//! dimension line (or arc) with arrowheads, and the value centred on it; radius and diameter
//! leaders with R / Ø, angles as an arc between the two lines, arc lengths with the arc mark,
//! driven (reference) dimensions in parentheses in a muted colour. The geometry comes from
//! `solvecraft_sketch::dim_layout`.
//! `solidcraft_sketch::dim_layout`.
//!
//! A click selects a dimension (Delete removes it), a drag moves its text (and so the offset of
//! its dimension line), a double-click edits its value in place with parameter completion.
@@ -14,9 +14,9 @@ use std::sync::Arc;
use egui::{FontId, Pos2, Rect, Shape, Stroke, vec2};
use serde_json::json;
use solvecraft_engine::Sel;
use solvecraft_engine::geom::Vec2;
use solvecraft_engine::sketch::{ConstraintKind, DimFrame, chain_centres, default_text, dim_frame, dim_layout};
use solidcraft_engine::Sel;
use solidcraft_engine::geom::Vec2;
use solidcraft_engine::sketch::{ConstraintKind, DimFrame, chain_centres, default_text, dim_frame, dim_layout};
use crate::SolveApp;
use crate::theme::Tokens;
@@ -52,16 +52,16 @@ struct Edit {
/// document also makes the editor's identity check safe across document replacements.
#[derive(Clone, Debug)]
struct DimensionDraft {
baseline: Arc<solvecraft_engine::doc::Document>,
baseline: Arc<solidcraft_engine::doc::Document>,
sketch: u64,
component: u64,
path: Option<Vec<u64>>,
pending: std::collections::BTreeMap<u64, solvecraft_engine::doc::Mat>,
pending: std::collections::BTreeMap<u64, solidcraft_engine::doc::Mat>,
entities: Vec<String>,
at: Vec2,
checked_expr: String,
preview_doc: Arc<solvecraft_engine::doc::Document>,
preview: Arc<solvecraft_engine::doc::ModelState>,
preview_doc: Arc<solidcraft_engine::doc::Document>,
preview: Arc<solidcraft_engine::doc::ModelState>,
error: Option<String>,
/// A dimension that would over-constrain the sketch, placed as a driven (reference) one:
/// it has no parameter and no value to edit (#320).
@@ -79,7 +79,7 @@ impl DimensionDraft {
fn params(&self, expr: &str) -> serde_json::Value {
json!({"entities": self.entities, "text_at": [self.at.x, self.at.y], "value": expr})
}
fn evaluate(&self, app: &SolveApp, expr: &str) -> Result<solvecraft_engine::Session, String> {
fn evaluate(&self, app: &SolveApp, expr: &str) -> Result<solidcraft_engine::Session, String> {
let mut scratch = app.session.scratch();
let result = scratch.execute("sketch.dimension", &self.params(expr)).map_err(|e| e.to_string())?;
let param = result.get("param").and_then(serde_json::Value::as_str).ok_or("The dimension value is unavailable")?;
@@ -169,11 +169,11 @@ fn draft_of(app: &SolveApp, sketch: u64, entities: &[String], at: Vec2, driven:
/// Cache inference at a stationary pointer, including incomplete/unsupported picks.
#[derive(Clone)]
struct Placement {
baseline: Arc<solvecraft_engine::doc::Document>,
baseline: Arc<solidcraft_engine::doc::Document>,
sketch: Option<u64>,
component: u64,
path: Option<Vec<u64>>,
pending: std::collections::BTreeMap<u64, solvecraft_engine::doc::Mat>,
pending: std::collections::BTreeMap<u64, solidcraft_engine::doc::Mat>,
entities: Vec<String>,
at: Vec2,
draft: Option<(String, DimensionDraft)>,
@@ -398,7 +398,7 @@ pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &P
let mut constraints = sk.constraints.iter().map(|c| (ss, c, false)).collect::<Vec<_>>();
if let (Some(source), Some((param, _))) = (draft_sketch, draft.as_ref()) {
// A driven draft is keyed by its constraint id (it has no parameter).
let ours = |c: &&solvecraft_engine::sketch::Constraint| c.param.as_deref() == Some(param.as_str()) || (c.driven && c.id == *param);
let ours = |c: &&solidcraft_engine::sketch::Constraint| c.param.as_deref() == Some(param.as_str()) || (c.driven && c.id == *param);
constraints.extend(source.sketch.constraints.iter().filter(ours).map(|c| (source, c, true)));
}
for (source, c, is_draft) in constraints {
@@ -483,7 +483,7 @@ pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &P
}
/// A point a dimension measures (for the shape it belongs to).
fn first_point(sk: &solvecraft_engine::sketch::Sketch, k: &ConstraintKind) -> Option<usize> {
fn first_point(sk: &solidcraft_engine::sketch::Sketch, k: &ConstraintKind) -> Option<usize> {
use ConstraintKind::*;
match *k {
Length { l, .. } => sk.curves.get(l).and_then(|c| c.kind.point_ids().first().copied()),
@@ -574,9 +574,9 @@ fn edit_box(app: &mut SolveApp, ctx: &egui::Context) {
.session
.doc
.param(&e.param)
.and_then(|p| solvecraft_engine::doc::expr::unit_info(&p.unit))
.and_then(|p| solidcraft_engine::doc::expr::unit_info(&p.unit))
.map(|(k, _)| k)
.unwrap_or(solvecraft_engine::doc::expr::Kind::Unitless);
.unwrap_or(solidcraft_engine::doc::expr::Kind::Unitless);
let expression = app.session.doc.with_design_unit(&e.expr, kind);
app.run("parameters.change", json!({"name": e.param, "expression": expression}))
};
+10 -10
View File
@@ -11,7 +11,7 @@ fn line(end: [f64; 2]) -> Harness {
// Leave room around the measured shape while testing the draft itself. Separate Fit
// tests exercise measured dock occlusion without changing the picking projection.
h.app.cam.distance = 80.0;
h.app.cam.target = solvecraft_engine::geom::Vec3::new(end[0] * 0.5, end[1] * 0.5, 0.0);
h.app.cam.target = solidcraft_engine::geom::Vec3::new(end[0] * 0.5, end[1] * 0.5, 0.0);
h.settle();
h
}
@@ -137,7 +137,7 @@ fn rectangle_side_and_fixed_origin_line_dimensions_preserve_other_constraints()
h.app.run("sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[20,10]})).unwrap();
h.call("ui.view", json!({"view":"top"}));
h.app.cam.distance = 80.;
h.app.cam.target = solvecraft_engine::geom::Vec3::new(10., 5., 0.);
h.app.cam.target = solidcraft_engine::geom::Vec3::new(10., 5., 0.);
h.settle();
let baseline = h.app.session.doc.to_json();
let undo = h.app.session.undo.len();
@@ -155,7 +155,7 @@ fn rectangle_side_and_fixed_origin_line_dimensions_preserve_other_constraints()
#[test]
fn pending_second_nested_plane_uses_world_picks_but_one_local_dimension() {
use solvecraft_engine::geom::Vec2;
use solidcraft_engine::geom::Vec2;
crate::dim_view::cancel();
let mut h = Harness::new();
let parent = h.app.run("component.create", json!({"name":"Assembly"})).unwrap();
@@ -221,7 +221,7 @@ fn invalid_zero_negative_failed_open_and_click_away_leave_no_dimension() {
h.step(&json!({"key":"Escape"})).unwrap();
}
place(&mut h, &[[10., 0.]], [10., 5.]);
assert!(h.app.run("doc.open", json!({"path":"missing-dimension-draft-design.solvecraft"})).is_err());
assert!(h.app.run("doc.open", json!({"path":"missing-dimension-draft-design.solidcraft"})).is_err());
assert!(crate::dim_view::editing(), "failed replacement must preserve the draft");
assert_eq!(h.app.session.doc.to_json(), baseline);
h.frames(8);
@@ -247,7 +247,7 @@ fn dimension_to_fillet_virtual_sharp_keeps_the_arc_and_fixed_origin() {
let corner = fillet["virtual_corners"][0].as_str().unwrap().to_string();
h.call("ui.view", json!({"view":"top"}));
h.app.cam.distance = 90.;
h.app.cam.target = solvecraft_engine::geom::Vec3::new(5., 10., 0.);
h.app.cam.target = solidcraft_engine::geom::Vec3::new(5., 10., 0.);
h.settle();
let baseline = h.app.session.doc.to_json();
let undo = h.app.session.undo.len();
@@ -262,7 +262,7 @@ fn dimension_to_fillet_virtual_sharp_keeps_the_arc_and_fixed_origin() {
let sk = &solved.sketch;
assert!((sk.point(sk.point_index(&corner).unwrap()).unwrap().y - 30.).abs() < 1e-6);
assert!(sk.point(sk.resolve_point("riser.start").unwrap()).unwrap().len() < 1e-8);
assert!(sk.curves.iter().any(|c| matches!(c.kind, solvecraft_engine::sketch::CurveKind::Arc { .. })), "the dimension retains the fillet arc");
assert!(sk.curves.iter().any(|c| matches!(c.kind, solidcraft_engine::sketch::CurveKind::Arc { .. })), "the dimension retains the fillet arc");
h.app.run("edit.undo", json!({})).unwrap();
assert_eq!(h.app.session.doc.to_json(), baseline);
}
@@ -276,7 +276,7 @@ fn draft_annotation_does_not_move_committed_dimension_hitboxes() {
let width = h.app.run("sketch.dimension", json!({"entities":["l3"],"value":20})).unwrap()["param"].as_str().unwrap().to_string();
h.call("ui.view", json!({"view":"top"}));
h.app.cam.distance = 80.;
h.app.cam.target = solvecraft_engine::geom::Vec3::new(10., 5., 0.);
h.app.cam.target = solidcraft_engine::geom::Vec3::new(10., 5., 0.);
h.settle();
let at = crate::dim_view::text_at(&width).unwrap();
let drawn = crate::dim_view::drawn();
@@ -317,8 +317,8 @@ fn inch_dimension_entry_survives_unit_changes_undo_and_reopen() {
assert_eq!(h.app.session.world_state().sketch(sketch).unwrap().sketch, before);
h.app.run("edit.undo", json!({})).unwrap();
assert_eq!(h.app.session.doc.units, "in");
let doc = solvecraft_engine::doc::Document::from_json(&h.app.session.doc.to_json()).unwrap();
let reopened = solvecraft_engine::Session::new(doc);
let doc = solidcraft_engine::doc::Document::from_json(&h.app.session.doc.to_json()).unwrap();
let reopened = solidcraft_engine::Session::new(doc);
assert_eq!(reopened.doc.units, "in");
assert_eq!(reopened.world_state().sketch(sketch).unwrap().sketch, before);
crate::dim_view::cancel();
@@ -329,7 +329,7 @@ fn inch_dimension_entry_survives_unit_changes_undo_and_reopen() {
#[test]
fn placing_a_two_point_dimension_picks_horizontal_vertical_or_aligned() {
// What each placement fixes at 10 mm: the X span, the Y span, or the length.
let spans = |a: solvecraft_engine::geom::Vec2, b: solvecraft_engine::geom::Vec2| [(b.x - a.x).abs(), (b.y - a.y).abs(), a.dist(b)];
let spans = |a: solidcraft_engine::geom::Vec2, b: solidcraft_engine::geom::Vec2| [(b.x - a.x).abs(), (b.y - a.y).abs(), a.dist(b)];
for (at, measured) in [([6., 25.], 0), ([-10., 1.], 1), ([-5., 12.], 2)] {
let mut h = line([12., 16.]);
place(&mut h, &[[0., 0.], [12., 16.]], at);
+7 -7
View File
@@ -6,8 +6,8 @@
use egui::{Align2, Color32, FontId, Rect, Sense, Stroke, pos2, vec2};
use serde_json::{Value, json};
use solvecraft_engine::Session;
use solvecraft_engine::render::Camera;
use solidcraft_engine::Session;
use solidcraft_engine::render::Camera;
use crate::SolveApp;
use crate::theme::Tokens;
@@ -555,7 +555,7 @@ mod tests {
fn tabs_keep_their_own_design() {
let mut a = app();
a.run("solid.box", json!({"length": 10, "width": 10, "height": 10})).unwrap();
a.session.selection = vec![solvecraft_engine::Sel::Body { name: "Body1".into() }];
a.session.selection = vec![solidcraft_engine::Sel::Body { name: "Body1".into() }];
a.cam.distance = 123.0;
new_design(&mut a);
assert_eq!(a.docs.count(), 2);
@@ -591,7 +591,7 @@ mod tests {
assert!(a.quit_requested, "nothing unsaved: quits at once");
let mut a = app();
let file = std::env::temp_dir().join(format!("solvecraft-quit-save-{}.solvecraft", std::process::id()));
let file = std::env::temp_dir().join(format!("solidcraft-quit-save-{}.solidcraft", std::process::id()));
let _ = std::fs::remove_file(&file);
a.run("solid.box", json!({"length": 10, "width": 10, "height": 10})).unwrap();
a.run("file.save_as", json!({"path": file.to_string_lossy()})).unwrap();
@@ -607,7 +607,7 @@ mod tests {
answer_prompt(&mut a, Answer::Save);
assert!(!dirty(&a, 0), "saved");
assert_eq!(
std::fs::read(&file).ok().and_then(|b| solvecraft_engine::io::read_design(&b).ok()).map(|d| d.features.len()),
std::fs::read(&file).ok().and_then(|b| solidcraft_engine::io::read_design(&b).ok()).map(|d| d.features.len()),
Some(2),
"the file holds the change"
);
@@ -638,7 +638,7 @@ mod tests {
assert!(!a.quit_requested && a.docs.quitting.is_none() && a.session.is_dirty());
// A picked file: saved, then quit.
let file = std::env::temp_dir().join(format!("solvecraft-quit-saveas-{}.solvecraft", std::process::id()));
let file = std::env::temp_dir().join(format!("solidcraft-quit-saveas-{}.solidcraft", std::process::id()));
let _ = std::fs::remove_file(&file);
let to = file.to_string_lossy().to_string();
a.services.pick_save = Some(Box::new(move |_: &str, _: &[&str]| Some(to.clone())));
@@ -653,7 +653,7 @@ mod tests {
fn adopt_reuses_a_blank_design() {
let mut a = app();
let mut s = Session::default();
s.run_script(&solvecraft_engine::sample::script()).unwrap();
s.run_script(&solidcraft_engine::sample::script()).unwrap();
adopt(&mut a, s);
assert_eq!(a.docs.count(), 1);
assert!(!a.session.doc.features.is_empty());
+2 -2
View File
@@ -8,8 +8,8 @@ use std::cell::{Cell, RefCell};
use egui::{Align2, FontId, Shape, Stroke};
use serde_json::{Value, json};
use solvecraft_engine::geom::Vec2;
use solvecraft_engine::sketch::{CurveKind, Sketch};
use solidcraft_engine::geom::Vec2;
use solidcraft_engine::sketch::{CurveKind, Sketch};
use crate::SolveApp;
use crate::theme::Tokens;
+1 -1
View File
@@ -168,7 +168,7 @@ mod tests {
impl Editor {
fn new(text: &str) -> Self {
let ctx = egui::Context::default();
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
for (name, value) in [("width", 40), ("width_extra", 50), ("wing", 20), ("wire", 10)] {
app.run("parameters.add", json!({"name":name,"expression":format!("{value} mm")})).unwrap();
}
+9 -9
View File
@@ -1,7 +1,7 @@
//! Exercise the actual feature editor, including frames rendered before OK.
use crate::scenario::Harness;
use serde_json::{Value, json};
use solvecraft_engine::doc::{Extent, FeatureKind};
use solidcraft_engine::doc::{Extent, FeatureKind};
fn make(params: Value) -> Harness {
make_in(Harness::new(), params)
@@ -31,12 +31,12 @@ fn unchanged_all_profiles_includes_profiles_added_after_edit() {
open(&mut h);
h.step(&json!({"widget":"OK"})).unwrap();
let FeatureKind::Extrude { profiles, .. } = &h.app.session.doc.find_feature("Depth").unwrap().kind else { panic!("extrude") };
assert!(matches!(profiles, solvecraft_engine::doc::ProfileSel::All));
assert!(matches!(profiles, solidcraft_engine::doc::ProfileSel::All));
h.app.run("sketch.edit", json!({"sketch":"Base"})).unwrap();
h.app.run("sketch.rectangle.two_point", json!({"p0":[20,0],"p1":[25,5]})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
let state = h.app.session.world_state();
let volume: f64 = state.bodies.iter().map(|b| solvecraft_engine::doc::kernel::measure(&b.body).unwrap().volume).sum();
let volume: f64 = state.bodies.iter().map(|b| solidcraft_engine::doc::kernel::measure(&b.body).unwrap().volume).sum();
assert!((volume - 1250.0).abs() < 1e-7, "{volume}");
}
@@ -71,7 +71,7 @@ fn negative_edit_keeps_downstream_distance_reference_positive() {
h.app.run("solid.box", json!({"length":10,"width":10,"height":"d1","corner":[30,0,0],"body_name":"Dependent"})).unwrap();
open(&mut h);
h.step(&json!({"widget":"OK"})).unwrap();
assert_eq!(h.app.session.doc.eval("d1", solvecraft_engine::doc::expr::Kind::Length).unwrap(), 10.0);
assert_eq!(h.app.session.doc.eval("d1", solidcraft_engine::doc::expr::Kind::Length).unwrap(), 10.0);
assert_eq!(h.app.session.world_state().body("Dependent").unwrap().mesh().bounds().max.z, 10.0);
}
@@ -149,7 +149,7 @@ fn referenced_offset_recomputes_after_edit_and_save_reopen() {
assert_eq!(extent(&h).to_offset.as_deref(), Some("clearance"));
h.app.run("parameters.change", json!({"name":"clearance","expression":"6 mm"})).unwrap();
assert_eq!(h.app.session.world_state().body("Main").unwrap().mesh().bounds().max.z, 21.0);
let path = h.dir.join("extent.solvecraft");
let path = h.dir.join("extent.solidcraft");
h.app.run("file.save", json!({"path":path})).unwrap();
h.app.run("doc.open", json!({"path":path})).unwrap();
assert_eq!(extent(&h).to_offset.as_deref(), Some("clearance"));
@@ -174,8 +174,8 @@ fn explicit_reverse_keeps_original_expression_and_parameter_sign() {
h.step(&json!({"widget":"Reverse Direction"})).unwrap();
h.step(&json!({"widget":"OK"})).unwrap();
assert_eq!(extent(&h).distance, "10");
assert_eq!(extent(&h).direction, solvecraft_engine::doc::Direction::Negative);
assert_eq!(h.app.session.doc.eval("d1", solvecraft_engine::doc::expr::Kind::Length).unwrap(), 10.0);
assert_eq!(extent(&h).direction, solidcraft_engine::doc::Direction::Negative);
assert_eq!(h.app.session.doc.eval("d1", solidcraft_engine::doc::expr::Kind::Length).unwrap(), 10.0);
assert_eq!(h.app.session.world_state().body("Main").unwrap().mesh().bounds().min.z, -10.0);
}
@@ -191,7 +191,7 @@ fn failed_edit_and_cancel_keep_full_history_and_existing_redo() {
let redo = h.app.session.redo.len();
assert_eq!(undo, 199);
assert_eq!(redo, 1);
h.app.session.selection = vec![solvecraft_engine::Sel::Body { name: "Main".into() }];
h.app.session.selection = vec![solidcraft_engine::Sel::Body { name: "Main".into() }];
let selection = h.app.session.selection.clone();
let original = serde_json::to_value(&*h.app.session.doc).unwrap();
open(&mut h);
@@ -225,7 +225,7 @@ fn tapered_from_and_signed_to_have_correct_start_volume_and_preview_parity() {
let bounds = body.mesh().bounds();
assert!((bounds.min.z - lo).abs() < 1e-8 && (bounds.max.z - hi).abs() < 1e-8, "{params}: {bounds:?}");
let expected = 10.0 * (100.0 + 120.0 + 144.0) / 3.0;
let volume = solvecraft_engine::doc::kernel::measure(&body.body).unwrap().volume;
let volume = solidcraft_engine::doc::kernel::measure(&body.body).unwrap().volume;
assert!((volume - expected).abs() < 1e-5, "{params}: volume{volume} expected{expected}");
let original = extent(&h);
open(&mut h);
+3 -3
View File
@@ -11,7 +11,7 @@ use std::sync::{
use serde_json::Value;
#[cfg(not(target_arch = "wasm32"))]
use serde_json::json;
use solvecraft_engine::{Session, doc::Document};
use solidcraft_engine::{Session, doc::Document};
use crate::SolveApp;
@@ -102,14 +102,14 @@ fn start(app: &mut SolveApp) {
#[cfg(not(target_arch = "wasm32"))]
fn compute(mut session: Session, path: &str, insert: bool, phase: &AtomicU8, cancelled: &AtomicBool) -> Job {
let profile = std::env::var_os("SOLVECRAFT_PROFILE_OPEN").is_some();
let profile = solidcraft_engine::geom::env::var_os("PROFILE_OPEN").is_some();
let began = std::time::Instant::now();
if cancelled.load(Ordering::Relaxed) {
return Err("file operation cancelled".into());
}
let command = if !insert {
"doc.open"
} else if solvecraft_engine::io::is_mesh_path(path) {
} else if solidcraft_engine::io::is_mesh_path(path) {
"file.insert_mesh"
} else {
"file.insert_step"
+1 -1
View File
@@ -343,7 +343,7 @@ pub fn setup_fonts(ctx: &egui::Context) {
}
/// On web, setup_fonts does not perform synchronous disk reads.
/// Use asynchronous loading through [`solvecraft_web`] or host scripts.
/// Use asynchronous loading through [`solidcraft_web`] or host scripts.
#[cfg(target_arch = "wasm32")]
pub fn setup_fonts(_ctx: &egui::Context) {}
+2 -2
View File
@@ -7,7 +7,7 @@
use std::cell::RefCell;
use egui::{Color32, Pos2, Rect, Sense, Shape, Stroke, vec2};
use solvecraft_engine::geom::Vec3;
use solidcraft_engine::geom::Vec3;
use crate::theme::Tokens;
use crate::viewport::{Proj, projected_polygon};
@@ -426,7 +426,7 @@ mod tests {
let limit = (0.45 * 2f64.sqrt() * LEN_PX) as f32 + 3.0;
let mut worst = 0.0f32;
for dir in crate::viewport::orbit_directions() {
let cam = solvecraft_engine::render::Camera::default().looking_from(dir);
let cam = solidcraft_engine::render::Camera::default().looking_from(dir);
let proj = Proj { cam, vp: cam.view_proj(4.0 / 3.0, 100.0), rect };
let l = mm_per_px(&proj, t.center).unwrap() * LEN_PX;
let sh = shapes(&proj, &t, l).unwrap();
+6 -6
View File
@@ -1,5 +1,5 @@
//! The Help menu (the ? button on the application bar): Documentation (the online book), About
//! SolveCraft (version, commit, build date, licences, attributions, fonts), the keyboard shortcuts
//! SolidCraft (version, commit, build date, licences, attributions, fonts), the keyboard shortcuts
//! (read only), and Report an Issue, which opens a local editable report draft.
use egui::{RichText, vec2};
@@ -8,12 +8,12 @@ use crate::SolveApp;
use crate::theme::Tokens;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const COMMIT: &str = env!("SOLVECRAFT_COMMIT");
pub const BUILD_DATE: &str = env!("SOLVECRAFT_BUILD_DATE");
pub const COMMIT: &str = env!("SOLIDCRAFT_COMMIT");
pub const BUILD_DATE: &str = env!("SOLIDCRAFT_BUILD_DATE");
/// The user and developer guide (docs/book, published by .github/workflows/pages.yml).
pub const DOCS_URL: &str = "https://bherbruck.github.io/solidcraft/";
const ATTRIBUTION: &str = include_str!("../../../ATTRIBUTION.md");
use solvecraft_engine::licences::{LICENSE_APACHE, LICENSE_MIT, NOTICE, THIRD_PARTY};
use solidcraft_engine::licences::{LICENSE_APACHE, LICENSE_MIT, NOTICE, THIRD_PARTY};
/// Fonts: what this build carries.
pub const FONTS: &str = "The interface uses egui's built-in fonts (Ubuntu Light and Hack, under the Ubuntu Font Licence and \
the MIT/Bitstream Vera licence; Noto Emoji and the emoji icon font, under the OFL and MIT). Sketch text uses Liberation Sans \
@@ -137,7 +137,7 @@ fn about(app: &mut SolveApp, ctx: &egui::Context) {
});
egui::CollapsingHeader::new("Third-party licences").show(ui, |ui| {
if ui.button("Copy all licences").clicked() {
ctx.copy_text(solvecraft_engine::licences::all());
ctx.copy_text(solidcraft_engine::licences::all());
}
third_party(ui);
});
@@ -170,7 +170,7 @@ mod tests {
fn about_knows_its_build_and_diagnostics_say_where_it_runs() {
assert!(!COMMIT.is_empty() && BUILD_DATE.len() == 10, "{COMMIT} {BUILD_DATE}");
assert!(ATTRIBUTION.contains("ATTRIBUTION") || ATTRIBUTION.contains("Attribution"));
let app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
let d = diagnostics(&app);
assert!(d.contains(VERSION) && d.contains(std::env::consts::OS) && d.contains("Renderer: CPU"), "{d}");
}
+20 -20
View File
@@ -5,8 +5,8 @@
use std::collections::HashMap;
use egui::{Color32, CornerRadius, RichText, Sense, Stroke, vec2};
use solvecraft_engine::Session;
use solvecraft_engine::render::{Camera, StandardView};
use solidcraft_engine::Session;
use solidcraft_engine::render::{Camera, StandardView};
use crate::SolveApp;
use crate::theme::Tokens;
@@ -61,15 +61,15 @@ impl HomeState {
pub fn thumbnail(s: &Session) -> Option<egui::ColorImage> {
let mut cam = Camera::default();
cam.set_view(StandardView::Iso);
let b = solvecraft_engine::view::bounds(s);
let b = solidcraft_engine::view::bounds(s);
if b.is_empty() {
return None;
}
cam.fit(&b);
// Fill the card: the fit leaves room for a whole window's margins.
cam.distance *= 0.7;
let scene = solvecraft_engine::view::scene(s, &cam);
let c = solvecraft_engine::render::render(&scene, &cam, THUMB[0], THUMB[1]);
let scene = solidcraft_engine::view::scene(s, &cam);
let c = solidcraft_engine::render::render(&scene, &cam, THUMB[0], THUMB[1]);
(c.rgba.len() == c.w * c.h * 4).then(|| egui::ColorImage::from_rgba_unmultiplied([c.w, c.h], &c.rgba))
}
@@ -86,7 +86,7 @@ fn cache_path(key: &str) -> Option<std::path::PathBuf> {
.or_else(|| std::env::var_os("XDG_CACHE_HOME"))
.map(std::path::PathBuf::from)
.unwrap_or_else(std::env::temp_dir);
Some(root.join("SolveCraft").join("thumbnails").join(format!("{:016x}.png", h.finish())))
Some(root.join("SolidCraft").join("thumbnails").join(format!("{:016x}.png", h.finish())))
}
#[cfg(not(target_arch = "wasm32"))]
@@ -123,11 +123,11 @@ fn remember_at(s: &Session, #[cfg(not(target_arch = "wasm32"))] identity: Option
#[cfg(not(target_arch = "wasm32"))]
if let Some(path) = identity.filter(|p| s.path.as_deref().and_then(cache_path).as_ref() == Some(p)) {
let rgba: Vec<u8> = img.pixels.iter().flat_map(|c| c.to_srgba_unmultiplied()).collect();
if let Some(bytes) = solvecraft_engine::render::png(THUMB[0], THUMB[1], rgba)
if let Some(bytes) = solidcraft_engine::render::png(THUMB[0], THUMB[1], rgba)
&& let Some(dir) = path.parent()
&& std::fs::create_dir_all(dir).is_ok()
{
let _ = solvecraft_engine::io::write_atomic(&path, &bytes, false);
let _ = solidcraft_engine::io::write_atomic(&path, &bytes, false);
}
}
Some(img)
@@ -173,7 +173,7 @@ fn make(key: &str) -> Made {
}
let mut s = Session::default();
let built = match key.strip_prefix("sample:").and_then(|i| i.parse::<usize>().ok()) {
Some(i) => solvecraft_engine::sample::samples().get(i).is_some_and(|smp| {
Some(i) => solidcraft_engine::sample::samples().get(i).is_some_and(|smp| {
let ok = s.run_script(&(smp.script)()).is_ok();
s.doc_mut().name = smp.name.to_string();
s.undo.clear();
@@ -315,7 +315,7 @@ pub fn open_sample(app: &mut SolveApp, i: usize) {
}
pub fn open_recent(app: &mut SolveApp, path: &str) {
if !solvecraft_engine::io::vfs::exists(path) {
if !solidcraft_engine::io::vfs::exists(path) {
app.set_status(format!("{path} is no longer there"), true);
return;
}
@@ -327,7 +327,7 @@ pub fn open_recent(app: &mut SolveApp, path: &str) {
pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
poll(app, ui.ctx());
let t = Tokens::get();
let samples = solvecraft_engine::sample::samples();
let samples = solidcraft_engine::sample::samples();
// In the browser a picture blocks the page while it builds (no threads) and a kernel
// failure there can't be caught: samples are built when they are opened.
if !cfg!(target_arch = "wasm32") {
@@ -336,7 +336,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
}
}
let recent = app.home.recent.clone();
for p in recent.iter().filter(|p| solvecraft_engine::io::vfs::exists(p)) {
for p in recent.iter().filter(|p| solidcraft_engine::io::vfs::exists(p)) {
want(&mut app.home, p);
}
let mut action: Option<Box<dyn FnOnce(&mut SolveApp)>> = None;
@@ -393,8 +393,8 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
ui.add_space(24.0);
ui.horizontal_wrapped(|ui| {
ui.hyperlink_to("Documentation", crate::help::DOCS_URL);
ui.hyperlink_to("Report an Issue", "https://github.com/bherbruck/solvecraft/issues/new");
ui.hyperlink_to("What's new", "https://github.com/bherbruck/solvecraft/releases/latest");
ui.hyperlink_to("Report an Issue", "https://github.com/bherbruck/solidcraft/issues/new");
ui.hyperlink_to("What's new", "https://github.com/bherbruck/solidcraft/releases/latest");
if ui.link("Keyboard Shortcuts").clicked() {
app.keymap.open = true;
app.keymap.read_only = true;
@@ -412,7 +412,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
let Some(p) = recent.get(i) else { return };
let path = std::path::Path::new(p);
let name = path.file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or_else(|| p.clone());
let missing = !solvecraft_engine::io::vfs::exists(p);
let missing = !solidcraft_engine::io::vfs::exists(p);
let mut sub = if missing {
"missing".to_string()
} else if p.starts_with("/opfs/") {
@@ -555,7 +555,7 @@ mod tests {
#[test]
fn cached_recent_uses_a_separate_png_without_reading_or_changing_design_json() {
let nonce = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
let path = std::env::temp_dir().join(format!("solvecraft-thumb-{}-{nonce}.solvecraft", std::process::id()));
let path = std::env::temp_dir().join(format!("solidcraft-thumb-{}-{nonce}.solidcraft", std::process::id()));
// Deliberately invalid design: a cached recent must not parse/evaluate it.
let bytes = b"{\"thumbnail_test\":true}";
std::fs::write(&path, bytes).unwrap();
@@ -579,12 +579,12 @@ mod tests {
fn recent_list_is_newest_first_and_bounded() {
let mut h = HomeState::default();
for i in 0..20 {
h.add_recent(&format!("/d/{i}.solvecraft"));
h.add_recent(&format!("/d/{i}.solidcraft"));
}
h.add_recent("/d/5.solvecraft");
h.add_recent("/d/5.solidcraft");
assert_eq!(h.recent.len(), MAX_RECENT);
assert_eq!(h.recent.first().map(String::as_str), Some("/d/5.solvecraft"));
assert_eq!(h.recent.iter().filter(|p| p.as_str() == "/d/5.solvecraft").count(), 1);
assert_eq!(h.recent.first().map(String::as_str), Some("/d/5.solidcraft"));
assert_eq!(h.recent.iter().filter(|p| p.as_str() == "/d/5.solidcraft").count(), 1);
}
/// Samples build in the background and make a picture; picking one opens its design.
+2 -2
View File
@@ -7,8 +7,8 @@ use std::cell::RefCell;
use egui::{Shape, Stroke};
use serde_json::json;
use solvecraft_engine::geom::Vec2;
use solvecraft_engine::sketch::{CurveKind, Sketch};
use solidcraft_engine::geom::Vec2;
use solidcraft_engine::sketch::{CurveKind, Sketch};
use crate::SolveApp;
use crate::theme::Tokens;
+10 -10
View File
@@ -218,16 +218,16 @@ fn signature(app: &SolveApp, dialog: Option<&dialogs::Dialog>) -> u64 {
h.0.finish()
}
fn reportable(feature: &solvecraft_engine::doc::Feature) -> bool {
fn reportable(feature: &solidcraft_engine::doc::Feature) -> bool {
!matches!(
feature.kind,
solvecraft_engine::doc::FeatureKind::Sketch { .. }
| solvecraft_engine::doc::FeatureKind::Import { .. }
| solvecraft_engine::doc::FeatureKind::MeshImport { .. }
solidcraft_engine::doc::FeatureKind::Sketch { .. }
| solidcraft_engine::doc::FeatureKind::Import { .. }
| solidcraft_engine::doc::FeatureKind::MeshImport { .. }
)
}
fn feature_context(feature: &solvecraft_engine::doc::Feature) -> String {
fn feature_context(feature: &solidcraft_engine::doc::Feature) -> String {
format!("Feature {}: {}\n{}", feature.id, debug(&feature.name, 160), block(&json_context(&feature.kind, 2000)))
}
@@ -249,7 +249,7 @@ pub fn draft_for(app: &SolveApp, dialog: Option<&dialogs::Dialog>, error: Option
context.push_str("\nSelected items (including occurrence paths):\n");
context.push_str(&block(&debug(&app.session.selection.iter().take(32).collect::<Vec<_>>(), 3000)));
for item in app.session.selection.iter().take(8) {
if let solvecraft_engine::Sel::Feature { id } = item
if let solidcraft_engine::Sel::Feature { id } = item
&& let Some(feature) = app.session.doc.feature(*id)
&& reportable(feature)
{
@@ -289,7 +289,7 @@ pub fn draft_for(app: &SolveApp, dialog: Option<&dialogs::Dialog>, error: Option
Draft { title, body, image: None, texture: None, note: String::new(), source_signature: source_signature(app, dialog) }
}
/// A draft from a crash report (`solvecraft_engine::crash`): what happened, where, the command
/// A draft from a crash report (`solidcraft_engine::crash`): what happened, where, the command
/// and how to replay it. The design stays on this computer; the draft says how to share it.
pub fn crash_draft(report: &serde_json::Value) -> Draft {
let s = |k: &str| report.get(k).and_then(serde_json::Value::as_str).unwrap_or("?").to_string();
@@ -328,7 +328,7 @@ pub fn crash_draft(report: &serde_json::Value) -> Draft {
if recent.is_empty() { "(none recorded)".to_string() } else { recent.join(", ") },
s("id"),
match (report.get("design_path").is_some(), report.get("replay_path").is_some()) {
(true, true) => "the design as it was before the crash (design.solvecraft) and the commands that crash it (replay.json)",
(true, true) => "the design as it was before the crash (design.solidcraft) and the commands that crash it (replay.json)",
(false, true) => "the commands that crash it on a new design (replay.json)",
_ => "no replay (see its notes)",
},
@@ -622,7 +622,7 @@ mod tests {
#![allow(clippy::unwrap_used, clippy::panic)]
use super::*;
use serde_json::json;
use solvecraft_engine::{Sel, Session};
use solidcraft_engine::{Sel, Session};
fn app() -> SolveApp {
SolveApp::new(Session::default(), Default::default())
@@ -909,7 +909,7 @@ mod tests {
h.app.run("sketch.line", json!({"points":[[0,0],[20,0]]})).unwrap();
h.call("ui.view", json!({"view":"top"}));
h.app.cam.distance = 80.0;
h.app.cam.target = solvecraft_engine::geom::Vec3::new(10.0, 0.0, 0.0);
h.app.cam.target = solidcraft_engine::geom::Vec3::new(10.0, 0.0, 0.0);
h.settle();
h.step(&json!({"start":"sketch.dimension"})).unwrap();
for point in [[0, 0, 0], [20, 0, 0], [10, 5, 0]] {
+6 -6
View File
@@ -7,7 +7,7 @@ use std::collections::BTreeMap;
use egui::RichText;
use serde_json::{Value, json};
use solvecraft_engine::command_specs;
use solidcraft_engine::command_specs;
use crate::SolveApp;
use crate::theme::Tokens;
@@ -63,7 +63,7 @@ pub fn effective(app: &SolveApp, id: &str) -> Option<String> {
match app.keymap.custom.get(id) {
Some(k) if k.is_empty() => None,
Some(k) => Some(k.clone()),
None => solvecraft_engine::find_command(id).and_then(|c| c.shortcut).map(str::to_string),
None => solidcraft_engine::find_command(id).and_then(|c| c.shortcut).map(str::to_string),
}
}
@@ -74,20 +74,20 @@ pub fn commands_for(app: &SolveApp, key: &str) -> Vec<&'static str> {
/// The enabled command a key press runs, if any.
pub fn command_for(app: &SolveApp, key: &str) -> Option<&'static str> {
commands_for(app, key).into_iter().find(|id| solvecraft_engine::find_command(id).is_some_and(|c| c.info(&app.session).enabled))
commands_for(app, key).into_iter().find(|id| solidcraft_engine::find_command(id).is_some_and(|c| c.info(&app.session).enabled))
}
/// Bind `key` to a command (`""` clears it). Refused for reserved keys, and when other commands
/// use the key unless `replace` (they lose it).
pub fn bind(app: &mut SolveApp, id: &str, key: &str, replace: bool) -> Result<(), String> {
let spec = solvecraft_engine::find_command(id).ok_or_else(|| format!("no command `{id}`"))?;
let spec = solidcraft_engine::find_command(id).ok_or_else(|| format!("no command `{id}`"))?;
if RESERVED.contains(&key) {
return Err(format!("{key} is used by SolidCraft itself"));
}
if !key.is_empty() {
let others: Vec<&'static str> = commands_for(app, key).into_iter().filter(|o| *o != id).collect();
if !others.is_empty() && !replace {
let names: Vec<&str> = others.iter().filter_map(|o| solvecraft_engine::find_command(o).map(|c| c.label)).collect();
let names: Vec<&str> = others.iter().filter_map(|o| solidcraft_engine::find_command(o).map(|c| c.label)).collect();
return Err(format!("{key} is already used by {}", names.join(", ")));
}
for o in others {
@@ -264,7 +264,7 @@ mod tests {
use super::*;
fn app() -> SolveApp {
SolveApp::new(solvecraft_engine::Session::default(), Default::default())
SolveApp::new(solidcraft_engine::Session::default(), Default::default())
}
#[test]
+26 -26
View File
@@ -1,6 +1,6 @@
//! The SolveCraft egui front end.
//! The SolidCraft egui front end.
//!
//! A thin shell over [`solvecraft_engine::Session`]: panels read engine state and act through
//! A thin shell over [`solidcraft_engine::Session`]: panels read engine state and act through
//! commands ([`SolveApp::run`]). Nothing here owns design data. The layout follows the familiar
//! parametric-CAD desktop: application bar, a toolbar of workspace tabs and panels, the browser
//! on the left, the 3D viewport with an axis view and navigation bar, the command dialog on the
@@ -55,7 +55,7 @@ mod status_tests {
"Shell: offset: must be positive"
);
assert_eq!(super::friendly_error("edit.undo", "nothing to undo"), "Undo: nothing to undo");
let mut app = super::SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = super::SolveApp::new(solidcraft_engine::Session::default(), Default::default());
app.run("solid.box", serde_json::json!({"length": 5, "width": 5, "height": 5})).unwrap();
assert_eq!(app.session.log.last().map(String::as_str), Some("Box done"));
let _ = app.run("edit.redo", serde_json::json!({}));
@@ -115,8 +115,8 @@ use std::sync::mpsc::Receiver;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use solvecraft_engine::Session;
use solvecraft_engine::render::{Camera, CameraAnim, StandardView};
use solidcraft_engine::Session;
use solidcraft_engine::render::{Camera, CameraAnim, StandardView};
pub use control::{ControlRequest, ControlResponse};
@@ -144,7 +144,7 @@ pub struct UiState {
/// A soft shadow on the ground under the model (seen from above).
pub ground_shadow: bool,
/// The world's up axis (Z or Y; #3): the camera, axis view, home view and ground follow it.
pub up_axis: solvecraft_engine::render::UpAxis,
pub up_axis: solidcraft_engine::render::UpAxis,
/// Sketches whose closed profiles are not shaded.
pub hidden_profiles: Vec<u64>,
/// Finished sketches whose dimensions are shown.
@@ -237,9 +237,9 @@ pub struct SolveApp {
pub status: Option<(String, f64, bool)>,
pub quit_requested: bool,
/// Autosave into the recovery folder (the desktop app turns it on).
pub autosave: Option<solvecraft_engine::recovery::Autosaver>,
pub autosave: Option<solidcraft_engine::recovery::Autosaver>,
/// Designs left by crashed apps, offered for recovery.
pub recoverable: Vec<solvecraft_engine::recovery::Entry>,
pub recoverable: Vec<solidcraft_engine::recovery::Entry>,
/// The bodies of a just-opened Y-up file, offered to be stood up (`stand_up`).
pub stand_up: Option<Vec<String>>,
/// The anti-aliasing sample counts the graphics device supports (empty without a GPU).
@@ -267,7 +267,7 @@ pub struct SolveApp {
shot_token: u64,
styled: bool,
fitted: bool,
pending_fit: Option<(Camera, solvecraft_engine::geom::Aabb3, bool)>,
pending_fit: Option<(Camera, solidcraft_engine::geom::Aabb3, bool)>,
/// The start page.
pub home: home::HomeState,
pub file_load: file_load::FileLoad,
@@ -538,7 +538,7 @@ impl SolveApp {
}
// Press Pull: edges get a fillet, faces and profiles an extrude.
if id == "solid.press_pull" {
let edges = self.session.selection.iter().any(|s| matches!(s, solvecraft_engine::Sel::Edge { .. }));
let edges = self.session.selection.iter().any(|s| matches!(s, solidcraft_engine::Sel::Edge { .. }));
return self.start(if edges { "solid.fillet" } else { "solid.extrude" });
}
// Construction toggle: selected sketch curves switch at once, otherwise pick them.
@@ -547,7 +547,7 @@ impl SolveApp {
sketch_palette::toggle_construction(self);
return;
}
if let Some(spec) = solvecraft_engine::find_command(id)
if let Some(spec) = solidcraft_engine::find_command(id)
&& (tools::Tool::for_command(id).is_some() || dialogs::Dialog::for_command(self, id).is_some())
&& id != "sketch.create"
{
@@ -580,7 +580,7 @@ impl SolveApp {
| "sketch.constraint.tangent"
| "sketch.constraint.concentric"
| "sketch.constraint.collinear"
) && let [solvecraft_engine::Sel::SketchCurve { id: a }, solvecraft_engine::Sel::SketchCurve { id: b }] =
) && let [solidcraft_engine::Sel::SketchCurve { id: a }, solidcraft_engine::Sel::SketchCurve { id: b }] =
self.session.selection.as_slice()
&& a != b
{
@@ -597,7 +597,7 @@ impl SolveApp {
.selection
.iter()
.filter_map(|s| match s {
solvecraft_engine::Sel::SketchCurve { id } | solvecraft_engine::Sel::SketchPoint { id } => Some(id.clone()),
solidcraft_engine::Sel::SketchCurve { id } | solidcraft_engine::Sel::SketchPoint { id } => Some(id.clone()),
_ => None,
})
.take(2)
@@ -622,7 +622,7 @@ impl SolveApp {
/// Edit a feature the way Fusion does: sketches open in sketch mode; other features roll the
/// timeline back to just before themselves and reopen their dialog, filled in.
pub fn edit_feature(&mut self, id: u64) {
use solvecraft_engine::doc::FeatureKind;
use solidcraft_engine::doc::FeatureKind;
let Some(f) = self.session.doc.feature(id) else { return };
if matches!(f.kind, FeatureKind::Sketch { .. }) {
self.edit_sketch(id);
@@ -652,7 +652,7 @@ impl SolveApp {
/// puts them back. Editing the sketch that is already open does nothing; editing another one
/// finishes the current sketch first, so edit sessions never nest.
pub fn edit_sketch(&mut self, id: u64) {
if let solvecraft_engine::doc::FaceAttachmentStatus::Lost { message, .. } =
if let solidcraft_engine::doc::FaceAttachmentStatus::Lost { message, .. } =
self.session.placed_document().sketch_face_attachment(id, &self.session.model)
{
browser::redefine_sketch(self, id);
@@ -704,7 +704,7 @@ impl SolveApp {
}
/// Everything shown as selected: the selection plus the open dialog's inputs.
pub fn highlighted(&self) -> Vec<solvecraft_engine::Sel> {
pub fn highlighted(&self) -> Vec<solidcraft_engine::Sel> {
let mut v = self.session.selection.clone();
if let Some(d) = &self.dialog {
v.extend(d.items());
@@ -744,7 +744,7 @@ impl SolveApp {
/// Insert a STEP file's bodies (an Import base feature) or a 3MF/STL file's meshes into the
/// current design.
pub fn insert_path(&mut self, path: &str) {
let cmd = if solvecraft_engine::io::is_mesh_path(path) { "file.insert_mesh" } else { "file.insert_step" };
let cmd = if solidcraft_engine::io::is_mesh_path(path) { "file.insert_mesh" } else { "file.insert_step" };
let before: Vec<String> = self.session.world_state().bodies.iter().map(|b| b.name.clone()).collect();
if let Ok(r) = self.run(cmd, json!({ "path": path })) {
self.fit_view();
@@ -758,7 +758,7 @@ impl SolveApp {
/// A dropped file: a STEP, 3MF or STL file joins a design that has features, anything else
/// opens.
pub fn drop_path(&mut self, path: &str) {
let import = solvecraft_engine::io::is_step_path(path) || solvecraft_engine::io::is_mesh_path(path);
let import = solidcraft_engine::io::is_step_path(path) || solidcraft_engine::io::is_mesh_path(path);
if import && !self.session.doc.features.is_empty() {
self.insert_path_async(path);
} else {
@@ -769,7 +769,7 @@ impl SolveApp {
/// Frame the model at once (programmatic use; the toolbar animates).
/// Make `up` the world's up axis: the camera turns to the same standard view in that world
/// (home) and frames the model.
pub fn set_up_axis(&mut self, up: solvecraft_engine::render::UpAxis) {
pub fn set_up_axis(&mut self, up: solidcraft_engine::render::UpAxis) {
self.ui.up_axis = up;
self.cam_anim = None;
self.cam.up = up;
@@ -781,16 +781,16 @@ impl SolveApp {
self.cam_anim = None;
self.viewport.orbit_snap = None;
self.viewport.orbit_free = None;
let bounds = solvecraft_engine::view::bounds(&self.session);
let bounds = solidcraft_engine::view::bounds(&self.session);
self.pending_fit = Some((self.cam, bounds, false));
self.cam = self.fitted(self.cam);
}
/// `cam` moved to frame the whole model.
pub fn fitted(&self, mut cam: Camera) -> Camera {
let b = solvecraft_engine::view::bounds(&self.session);
let b = solidcraft_engine::view::bounds(&self.session);
if b.is_empty() {
cam.target = solvecraft_engine::geom::Vec3::ZERO;
cam.target = solidcraft_engine::geom::Vec3::ZERO;
cam.distance = 150.0;
} else {
cam = self.fitted_bounds(cam, &b);
@@ -798,7 +798,7 @@ impl SolveApp {
cam
}
fn fitted_bounds(&self, mut cam: Camera, bounds: &solvecraft_engine::geom::Aabb3) -> Camera {
fn fitted_bounds(&self, mut cam: Camera, bounds: &solidcraft_engine::geom::Aabb3) -> Camera {
if let Some(layout) = &self.viewport.layout {
return viewport_layout::fit_bounds(cam, bounds, layout, self.ui.perspective).unwrap_or(cam);
}
@@ -857,13 +857,13 @@ impl SolveApp {
to = self.fitted_bounds(to, &b);
}
None => {
self.pending_fit = Some((to, solvecraft_engine::view::bounds(&self.session), true));
self.pending_fit = Some((to, solidcraft_engine::view::bounds(&self.session), true));
to = self.fitted(to);
}
},
"home" => {
to.set_view(StandardView::Iso);
self.pending_fit = Some((to, solvecraft_engine::view::bounds(&self.session), true));
self.pending_fit = Some((to, solidcraft_engine::view::bounds(&self.session), true));
to = self.fitted(to);
}
v => match StandardView::parse(v) {
@@ -1116,7 +1116,7 @@ impl SolveApp {
/// A command's name as people see it (its label), for the status bar.
pub fn command_label(id: &str) -> &str {
solvecraft_engine::find_command(id).map_or(id, |c| c.label)
solidcraft_engine::find_command(id).map_or(id, |c| c.label)
}
/// An engine error with the command's label instead of its id: "Shell: offset must be …".
+18 -18
View File
@@ -2,14 +2,14 @@
use egui::pos2;
use serde_json::json;
use solvecraft_engine::Session;
use solidcraft_engine::Session;
use crate::context_menu::{self, Item, Target};
use crate::{Services, SolveApp};
fn sample_app() -> SolveApp {
let mut s = Session::default();
s.run_script(&solvecraft_engine::sample::script()).unwrap();
s.run_script(&solidcraft_engine::sample::script()).unwrap();
SolveApp::new(s, Services::default())
}
@@ -19,7 +19,7 @@ fn properties_leave_with_the_document_but_survive_a_failed_open() {
app.menu.props = Some(("Prior body".into(), json!({"volume_mm3":42})));
app.start("config.table");
assert!(app.dialog.is_some());
assert!(app.run("doc.open", json!({"path":"__missing_properties_regression__.solvecraft"})).is_err());
assert!(app.run("doc.open", json!({"path":"__missing_properties_regression__.solidcraft"})).is_err());
assert!(app.menu.props.is_some());
assert!(app.dialog.is_some());
app.run("file.new", json!({"name":"New design"})).unwrap();
@@ -73,7 +73,7 @@ fn appearance_browser_action_targets_the_clicked_body() {
act(&mut app, &Target::Body { name: b.clone() }, "Appearance");
let dialog = app.dialog.as_ref().unwrap();
assert!(matches!(dialog.kind, crate::dialogs::Kind::Appearance(_)));
assert_eq!(dialog.inputs[0].items, vec![solvecraft_engine::Sel::Body { name: b }]);
assert_eq!(dialog.inputs[0].items, vec![solidcraft_engine::Sel::Body { name: b }]);
}
#[test]
@@ -102,7 +102,7 @@ fn face_menu_hides_and_finds_the_body() {
}
assert!(find(&items, "Appearance").enabled);
context_menu::run_item(&mut app, &find(&items, "Find in Timeline"), pos2(0.0, 0.0));
assert!(matches!(app.session.selection.as_slice(), [solvecraft_engine::Sel::Feature { .. }]));
assert!(matches!(app.session.selection.as_slice(), [solidcraft_engine::Sel::Feature { .. }]));
context_menu::run_item(&mut app, &find(&items, "Show/Hide"), pos2(0.0, 0.0));
assert_eq!(app.session.visibility.hidden_bodies, vec![b]);
}
@@ -268,7 +268,7 @@ fn browser_folds_are_kept_in_the_preferences() {
#[test]
fn delete_key_handles_every_kind_and_asks_about_dependents() {
use solvecraft_engine::Sel;
use solidcraft_engine::Sel;
let mut app = sample_app();
app.run("construct.plane.offset", json!({"base": "XY", "offset": 10})).unwrap();
app.run("solid.box", json!({"length": 5, "width": 5, "height": 5, "corner": [200, 0, 0]})).unwrap();
@@ -309,7 +309,7 @@ fn delete_key_handles_every_kind_and_asks_about_dependents() {
fn menu_delete_items_use_the_same_path() {
let mut app = sample_app();
let b = body(&app);
let original_volume = solvecraft_engine::kernel::measure(&app.session.world_state().body(&b).unwrap().body).unwrap().volume;
let original_volume = solidcraft_engine::kernel::measure(&app.session.world_state().body(&b).unwrap().body).unwrap().volume;
let created = app.run("component.from_bodies", json!({"bodies": [b]})).unwrap();
let c = app.session.doc.components[0].id;
let transfer = created["components"][0]["feature"].as_u64().unwrap();
@@ -328,14 +328,14 @@ fn menu_delete_items_use_the_same_path() {
assert_eq!(restored.bodies.len(), 1, "the original joined plate stays intact");
let plate = restored.body(&b).unwrap();
assert_eq!(plate.component, 0);
assert!((solvecraft_engine::kernel::measure(&plate.body).unwrap().volume - original_volume).abs() < 1e-6);
assert!(plate.mesh().contains(solvecraft_engine::geom::Vec3::new(40.0, 35.0, 12.0)));
let mut reopened = solvecraft_engine::Session::new(solvecraft_engine::doc::Document::from_json(&app.session.doc.to_json()).unwrap());
assert!((solidcraft_engine::kernel::measure(&plate.body).unwrap().volume - original_volume).abs() < 1e-6);
assert!(plate.mesh().contains(solidcraft_engine::geom::Vec3::new(40.0, 35.0, 12.0)));
let mut reopened = solidcraft_engine::Session::new(solidcraft_engine::doc::Document::from_json(&app.session.doc.to_json()).unwrap());
reopened.refresh();
let reopened_state = reopened.world_state();
assert_eq!(reopened_state.bodies.len(), 1);
assert_eq!(reopened_state.body(&b).unwrap().component, 0);
assert!((solvecraft_engine::kernel::measure(&reopened_state.body(&b).unwrap().body).unwrap().volume - original_volume).abs() < 1e-6);
assert!((solidcraft_engine::kernel::measure(&reopened_state.body(&b).unwrap().body).unwrap().volume - original_volume).abs() < 1e-6);
// A sketch from the browser: asks, since its features go too.
let id = app.session.doc.features.iter().find(|f| f.name == "Base").unwrap().id;
act(&mut app, &Target::Sketch { id }, "Delete");
@@ -349,7 +349,7 @@ fn canvas_menu_hides_renames_calibrates_and_deletes() {
png.extend(400u32.to_be_bytes());
png.extend(200u32.to_be_bytes());
png.extend([8, 6, 0, 0, 0, 0, 0, 0, 0]);
let data = solvecraft_engine::doc::canvas::base64_encode(&png);
let data = solidcraft_engine::doc::canvas::base64_encode(&png);
let id = app.run("canvas.insert", json!({"data": data, "plane": "XY", "width": 80})).unwrap()["canvas"].as_u64().unwrap();
let t = Target::Canvas { id };
act(&mut app, &t, "Show/Hide");
@@ -476,7 +476,7 @@ fn named_views_save_restore_rename_and_delete() {
act(&mut app, &Target::NamedViews, "New Named View");
assert_eq!(app.session.doc.named_views.len(), 1);
let saved = app.cam;
app.cam.set_view(solvecraft_engine::render::StandardView::Top);
app.cam.set_view(solidcraft_engine::render::StandardView::Top);
let t = Target::NamedView { name: "Named View1".into() };
act(&mut app, &t, "Restore");
assert_eq!(app.cam_anim.map(|a| a.to), Some(saved), "animates back to the saved camera");
@@ -523,7 +523,7 @@ fn an_uncaptured_move_asks_before_the_next_command() {
crate::browser::answer_capture(&mut app, "revert").unwrap();
assert!(app.session.pending_moves.is_empty());
assert!(app.dialog.is_some(), "Create Sketch started");
assert_eq!(app.session.doc.occurrences[0].transform, solvecraft_engine::doc::IDENTITY);
assert_eq!(app.session.doc.occurrences[0].transform, solidcraft_engine::doc::IDENTITY);
// Capture keeps the move.
app.dialog = None;
app.run("occurrence.move", json!({"occurrence": occ, "translate": [10, 0, 0], "capture": false})).unwrap();
@@ -552,21 +552,21 @@ fn occurrence_moves_turn_about_their_origin_then_move() {
app.run("component.from_bodies", json!({"bodies": [b]})).unwrap();
let occ = app.session.doc.occurrences[0].id;
let mut m = crate::browser::OccMove::new(occ);
m.pivot = Some(solvecraft_engine::geom::Vec3::new(5.0, 0.0, 0.0));
m.pivot = Some(solidcraft_engine::geom::Vec3::new(5.0, 0.0, 0.0));
m.translate = [10.0, 0.0, 0.0];
m.angles = [0.0, 0.0, 90.0];
crate::browser::apply_occurrence_move(&mut app, &m);
let t = *app.session.pending_moves.get(&occ).unwrap();
// The pivot lands 10 mm along X; X turns into Y.
let p = solvecraft_engine::doc::apply_point(&t, solvecraft_engine::geom::Vec3::new(5.0, 0.0, 0.0));
let x = solvecraft_engine::doc::apply_vector(&t, solvecraft_engine::geom::Vec3::X);
let p = solidcraft_engine::doc::apply_point(&t, solidcraft_engine::geom::Vec3::new(5.0, 0.0, 0.0));
let x = solidcraft_engine::doc::apply_vector(&t, solidcraft_engine::geom::Vec3::X);
assert!((p.x - 15.0).abs() < 1e-9 && p.y.abs() < 1e-9, "{p:?}");
assert!((x.y - 1.0).abs() < 1e-9, "{x:?}");
// Changing the values again starts from where it was, not from the last move.
m.translate = [0.0, 0.0, 0.0];
m.angles = [0.0; 3];
crate::browser::apply_occurrence_move(&mut app, &m);
assert_eq!(*app.session.pending_moves.get(&occ).unwrap(), solvecraft_engine::doc::IDENTITY);
assert_eq!(*app.session.pending_moves.get(&occ).unwrap(), solidcraft_engine::doc::IDENTITY);
}
#[test]
+5 -5
View File
@@ -1,7 +1,7 @@
use crate::context_menu::{self, Target};
use crate::scenario::Harness;
use serde_json::json;
use solvecraft_engine::Sel;
use solidcraft_engine::Sel;
#[test]
fn replaced_body_name_uses_evaluated_owner_in_browser_and_component_actions() {
@@ -104,19 +104,19 @@ fn occurrence_move_dialog_keeps_other_pending_moves_and_reuses_initial_baseline(
context_menu::run_item(&mut h.app, movement, egui::Pos2::ZERO);
let mut m = h.app.tree.occurrence_move.clone().unwrap();
m.pivot =
Some(solvecraft_engine::doc::apply_point(&h.app.session.occurrence_placement(&[second]).unwrap().world, solvecraft_engine::geom::Vec3::ZERO));
Some(solidcraft_engine::doc::apply_point(&h.app.session.occurrence_placement(&[second]).unwrap().world, solidcraft_engine::geom::Vec3::ZERO));
m.translate = [10.0, 0.0, 0.0];
crate::browser::apply_occurrence_move(&mut h.app, &m);
let at = h.app.session.occurrence_placement(&[second]).unwrap();
assert!((solvecraft_engine::doc::apply_point(&at.world, solvecraft_engine::geom::Vec3::ZERO).x - 65.0).abs() < 1e-8);
assert!((solidcraft_engine::doc::apply_point(&at.world, solidcraft_engine::geom::Vec3::ZERO).x - 65.0).abs() < 1e-8);
m.translate = [3.0, 0.0, 0.0];
crate::browser::apply_occurrence_move(&mut h.app, &m);
let at = h.app.session.occurrence_placement(&[second]).unwrap();
assert!((solvecraft_engine::doc::apply_point(&at.world, solvecraft_engine::geom::Vec3::ZERO).x - 58.0).abs() < 1e-8);
assert!((solidcraft_engine::doc::apply_point(&at.world, solidcraft_engine::geom::Vec3::ZERO).x - 58.0).abs() < 1e-8);
assert_eq!(h.app.session.pending_moves[&first], first_pending);
crate::browser::revert_occurrence_dialog(&mut h.app, &m);
let restored = h.app.session.occurrence_placement(&[second]).unwrap();
assert!((solvecraft_engine::doc::apply_point(&restored.world, solvecraft_engine::geom::Vec3::ZERO).x - 55.0).abs() < 1e-8);
assert!((solidcraft_engine::doc::apply_point(&restored.world, solidcraft_engine::geom::Vec3::ZERO).x - 55.0).abs() < 1e-8);
assert_eq!(h.app.session.pending_moves[&first], first_pending);
h.app.run("component.revert_position", json!({"occurrences":[second]})).unwrap();
context_menu::run_item(&mut h.app, movement, egui::Pos2::ZERO);
+3 -3
View File
@@ -3,7 +3,7 @@
use egui::{RichText, vec2};
use serde_json::Value;
use solvecraft_engine::command_specs;
use solidcraft_engine::command_specs;
use crate::SolveApp;
use crate::theme::Tokens;
@@ -38,7 +38,7 @@ pub fn submit(app: &mut SolveApp, line: &str) {
Some(i) => (&line[..i], line[i..].trim()),
None => (line, ""),
};
if solvecraft_engine::find_command(id).is_some() {
if solidcraft_engine::find_command(id).is_some() {
if rest.is_empty() {
app.start(id);
} else {
@@ -113,7 +113,7 @@ pub fn popup(app: &mut SolveApp, ctx: &egui::Context) {
}
egui::ScrollArea::vertical().max_height(280.0).show(ui, |ui| {
for (id, label, icon) in list.iter().take(60) {
let enabled = solvecraft_engine::find_command(id).is_some_and(|c| c.info(&app.session).enabled);
let enabled = solidcraft_engine::find_command(id).is_some_and(|c| c.info(&app.session).enabled);
ui.horizontal(|ui| {
let (ir, _) = ui.allocate_exact_size(vec2(18.0, 18.0), egui::Sense::hover());
crate::icons::paint(ui.painter(), ir, icon, t.icon, t.icon_fill, t.accent);
+2 -2
View File
@@ -69,7 +69,7 @@ fn rows(app: &mut SolveApp) -> Vec<Row> {
// Sketch dimension parameters: the sketch that owns each.
let mut owner: std::collections::BTreeMap<String, String> = Default::default();
for f in &app.session.doc.features {
if let solvecraft_engine::doc::FeatureKind::Sketch { sketch, .. } = &f.kind {
if let solidcraft_engine::doc::FeatureKind::Sketch { sketch, .. } = &f.kind {
for c in &sketch.constraints {
if let Some(p) = &c.param {
owner.insert(p.clone(), f.name.clone());
@@ -502,7 +502,7 @@ mod tests {
assert_eq!(word_before("1 + ", 4), (4, String::new()));
assert_eq!(word_before("ü", 9), (0, "ü".to_string()));
let ctx = egui::Context::default();
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
let _ = app.run("parameters.add", json!({"name": "width", "expression": "40 mm"}));
publish(&mut app, &ctx);
let s = suggestions(&ctx, "wi");
+48 -28
View File
@@ -11,7 +11,18 @@
use egui::{Modifiers, Pos2, RichText};
use serde::{Deserialize, Serialize};
use serde_json::json;
use solvecraft_engine::geom::Vec3;
use solidcraft_engine::geom::Vec3;
/// The key the apps keep the preferences under in eframe's storage.
pub const STORAGE_KEY: &str = "solidcraft.prefs";
/// The key earlier releases used. Compatibility shim (docs/compatibility.md): read when the
/// current key is missing, never written.
pub const LEGACY_STORAGE_KEY: &str = "solvecraft.prefs";
/// The stored preferences, from the current key or else the legacy one.
pub fn stored(get: impl Fn(&str) -> Option<String>) -> Option<String> {
get(STORAGE_KEY).or_else(|| get(LEGACY_STORAGE_KEY))
}
use crate::SolveApp;
use crate::theme::Tokens;
@@ -483,12 +494,12 @@ pub fn length_drag<'a>(value: &'a mut f64, units: &str, decimals: u8) -> egui::D
/// A length typed into a field, in millimetres: a bare number is in the design's units (`scale`
/// units per millimetre); an explicit unit ("10 mm", "0.5 in", "2 cm + 3 mm") is honoured.
pub fn parse_length_mm(text: &str, scale: f64) -> Option<f64> {
use solvecraft_engine::doc::expr;
use solidcraft_engine::doc::expr;
if !(scale.is_finite() && scale > 0.0) {
return None;
}
let defaults = expr::Defaults { len: 1.0 / scale, ..expr::Defaults::default() };
let no_params = |n: &str| Err(solvecraft_engine::doc::DocError::Expr(format!("unknown parameter `{n}`")));
let no_params = |n: &str| Err(solidcraft_engine::doc::DocError::Expr(format!("unknown parameter `{n}`")));
let value = expr::eval_with_defaults(text.trim(), &no_params, defaults).ok()?.to_kind_in(expr::Kind::Length, defaults).ok()?;
value.is_finite().then_some(value)
}
@@ -706,7 +717,7 @@ fn close(app: &mut SolveApp, revert: bool) {
/// The default orientation changed: the view turns to match.
fn up_axis_changed(app: &mut SolveApp) {
use solvecraft_engine::render::UpAxis;
use solidcraft_engine::render::UpAxis;
app.prefs_window.applied_up = Some(app.preferences.up_axis.clone());
let up = if app.preferences.up_axis.eq_ignore_ascii_case("y") { UpAxis::Y } else { UpAxis::Z };
if app.ui.up_axis != up || app.cam.up != up {
@@ -1001,6 +1012,15 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
#[cfg(test)]
mod tests {
#[test]
fn stored_prefs_fall_back_to_the_legacy_key() {
let only_old = |k: &str| (k == super::LEGACY_STORAGE_KEY).then(|| "old".to_string());
assert_eq!(super::stored(only_old).as_deref(), Some("old"));
let both = |k: &str| Some(if k == super::STORAGE_KEY { "new" } else { "old" }.to_string());
assert_eq!(super::stored(both).as_deref(), Some("new"));
assert_eq!(super::stored(|_| None), None);
}
use super::*;
#[test]
@@ -1009,7 +1029,7 @@ mod tests {
let _ = std::fs::create_dir_all(&dir);
let path = dir.join("graphics.json");
let services = crate::Services { graphics_path: Some(path.to_string_lossy().to_string()), ..Default::default() };
let mut app = SolveApp::new(solvecraft_engine::Session::default(), services);
let mut app = SolveApp::new(solidcraft_engine::Session::default(), services);
// WebGPU's guarantee for the surface format: 8x saved, the device can only do 1x and 4x.
app.msaa_supported = vec![1, 4];
app.preferences.msaa = 8;
@@ -1048,7 +1068,7 @@ mod tests {
#[test]
fn control_channel_rejects_msaa_the_device_cannot_do_and_bad_saved_msaa_loads_as_default() {
let mut app = SolveApp::new(solvecraft_engine::Session::default(), crate::Services::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
app.msaa_supported = vec![1, 4];
assert!(set(&mut app, &serde_json::json!({ "msaa": 8 })).is_err());
assert!(set(&mut app, &serde_json::json!({ "msaa": 1 })).is_ok());
@@ -1081,7 +1101,7 @@ mod tests {
h.call("ui.prefs", json!({"open":"Navigation"}));
h.step(&json!({"widget":"Radial context menu"})).unwrap();
assert!(h.app.preferences.radial_context_menu, "the actual checkbox enables radial menus");
let mut restored = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut restored = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
restored.load_prefs(&h.app.prefs());
assert!(restored.preferences.radial_context_menu, "enabled preference survives save/load");
h.call("ui.prefs", json!({"button":"cancel"}));
@@ -1097,7 +1117,7 @@ mod tests {
#[test]
fn wheel_smoothing_persists_and_follows_dialog_buttons() {
assert!(serde_json::from_value::<Prefs>(json!({})).unwrap().smooth_wheel_zoom);
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
control(&mut app, &json!({"open":"Navigation", "smooth_wheel_zoom":false})).unwrap();
control(&mut app, &json!({"button":"cancel"})).unwrap();
assert!(app.preferences.smooth_wheel_zoom);
@@ -1105,7 +1125,7 @@ mod tests {
control(&mut app, &json!({"button":"apply"})).unwrap();
control(&mut app, &json!({"smooth_wheel_zoom":true, "button":"cancel"})).unwrap();
assert!(!app.preferences.smooth_wheel_zoom);
let mut other = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut other = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
other.load_prefs(&app.prefs());
assert!(!other.preferences.smooth_wheel_zoom);
control(&mut app, &json!({"open":"Navigation", "button":"defaults"})).unwrap();
@@ -1116,14 +1136,14 @@ mod tests {
#[test]
fn openscad_preset_maps_navigation_and_persists() {
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
set(&mut app, &json!({"nav": OPENSCAD_NAV})).unwrap();
for button in [NavButton::Middle, NavButton::Right] {
assert_eq!(nav_mode(&app, button, Modifiers::default()), Some(NavMode::Pan));
assert_eq!(nav_mode(&app, button, Modifiers { shift: true, ..Default::default() }), Some(NavMode::Zoom));
}
assert_eq!(nav_mode(&app, NavButton::Left, Modifiers::default()), Some(NavMode::Orbit));
let mut other = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut other = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
other.load_prefs(&app.prefs());
assert_eq!(other.preferences.nav, OPENSCAD_NAV);
}
@@ -1132,14 +1152,14 @@ mod tests {
fn trackpad_pan_defaults_on_persists_and_follows_dialog_buttons() {
assert!(serde_json::from_value::<Prefs>(json!({})).unwrap().trackpad_pan);
assert!(Prefs::default().trackpad_pan);
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
control(&mut app, &json!({"open":"Navigation", "trackpad_pan":false})).unwrap();
control(&mut app, &json!({"button":"cancel"})).unwrap();
assert!(app.preferences.trackpad_pan);
control(&mut app, &json!({"open":"Navigation", "trackpad_pan":false})).unwrap();
control(&mut app, &json!({"button":"apply"})).unwrap();
assert!(!app.preferences.trackpad_pan);
let mut other = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut other = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
other.load_prefs(&app.prefs());
assert!(!other.preferences.trackpad_pan);
control(&mut app, &json!({"open":"Navigation", "button":"defaults"})).unwrap();
@@ -1150,7 +1170,7 @@ mod tests {
#[test]
fn presets_map_the_mouse_and_prefs_round_trip() {
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
let shift = Modifiers { shift: true, ..Default::default() };
let ctrl = Modifiers { ctrl: true, ..Default::default() };
let none = Modifiers::default();
@@ -1177,7 +1197,7 @@ mod tests {
assert!(set(&mut app, &json!({"default_units": "furlong"})).is_err());
assert!(set(&mut app, &json!({"nope": 1})).is_err());
// Kept between runs.
let mut other = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut other = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
other.load_prefs(&app.prefs());
assert_eq!(other.preferences, app.preferences);
assert_eq!(format_length(25.4, "in", 3), "1.000 in");
@@ -1187,7 +1207,7 @@ mod tests {
/// keeps them, Restore Defaults resets one page, and the shortcuts menus open its page.
#[test]
fn preferences_dialog_cancel_apply_and_defaults() {
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
control(&mut app, &json!({"open": "general"})).unwrap();
assert!(app.prefs_window.open && app.prefs_window.section == 0);
control(&mut app, &json!({"theme": "light", "ui_scale": 1.25, "zoom_to_cursor": false})).unwrap();
@@ -1205,7 +1225,7 @@ mod tests {
assert_eq!(app.preferences.nav, "alias", "Cancel goes back to the last Apply");
let to = {
let mut c = app.cam;
c.set_view(solvecraft_engine::render::StandardView::Top);
c.set_view(solidcraft_engine::render::StandardView::Top);
c
};
app.animate_to(to);
@@ -1232,7 +1252,7 @@ mod tests {
#[test]
fn language_preference_switches_and_persists() {
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
assert_eq!(app.preferences.language, "en");
assert_eq!(app.tr("ui.preferences"), "Preferences");
assert_eq!(app.tr("ui.extrude"), "Extrude");
@@ -1244,7 +1264,7 @@ mod tests {
assert_eq!(app.tr("ui.extrude"), "押し出し");
// Preferences round-trip
let mut other = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut other = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
other.load_prefs(&app.prefs());
assert_eq!(other.preferences.language, "ja");
assert_eq!(other.tr("ui.preferences"), "環境設定");
@@ -1253,18 +1273,18 @@ mod tests {
/// A STEP file opened Z-up is offered to be stood up: a quarter turn about X, undoable.
#[test]
fn a_y_up_step_file_can_be_stood_up() {
let dir = std::env::temp_dir().join(format!("solvecraft-standup-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("solidcraft-standup-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let path = dir.join("tall_in_y.step").to_string_lossy().to_string();
let mut src = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut src = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
src.run("solid.box", json!({"length": 10, "width": 40, "height": 5})).unwrap();
src.run("file.export", json!({"path": path})).unwrap();
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
app.open_path(&path);
let offered = app.stand_up.clone().unwrap_or_default();
assert_eq!(offered.len(), 1, "the file's body is offered");
let size = |app: &SolveApp| solvecraft_engine::view::bounds(&app.session);
let size = |app: &SolveApp| solidcraft_engine::view::bounds(&app.session);
assert!((size(&app).max.y - size(&app).min.y - 40.0).abs() < 1e-6);
control(&mut app, &json!({"stand_up": true})).unwrap();
let b = size(&app);
@@ -1274,11 +1294,11 @@ mod tests {
assert!((size(&app).max.y - size(&app).min.y - 40.0).abs() < 1e-6, "one undo step");
// Not offered when turned off, or with Y up as the default orientation.
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
app.preferences.offer_stand_up = false;
app.open_path(&path);
assert!(app.stand_up.is_none());
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
app.preferences.up_axis = "y".into();
app.open_path(&path);
assert!(app.stand_up.is_none());
@@ -1287,7 +1307,7 @@ mod tests {
#[test]
fn orbiting_around_the_cursor_keeps_that_point_still() {
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
app.run("solid.box", json!({"length": 40, "width": 40, "height": 40})).unwrap();
app.preferences.orbit_cursor = true;
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0));
@@ -1302,8 +1322,8 @@ mod tests {
/// The Y up preference turns the view: the camera's up axis follows it, startup included.
#[test]
fn up_axis_preference_turns_the_view() {
use solvecraft_engine::render::UpAxis;
let mut app = SolveApp::new(solvecraft_engine::Session::default(), crate::Services::default());
use solidcraft_engine::render::UpAxis;
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
let ctx = egui::Context::default();
apply(&mut app, &ctx);
assert_eq!(app.cam.up, UpAxis::Z);
+7 -7
View File
@@ -2,8 +2,8 @@
//! type, whether the command starts from a shortcut, the toolbar or a menu.
use serde_json::json;
use solvecraft_engine::geom::Vec3;
use solvecraft_engine::{Sel, Session};
use solidcraft_engine::geom::Vec3;
use solidcraft_engine::{Sel, Session};
use crate::dialogs::{Dialog, Kind, apply_commands};
use crate::{Services, SolveApp};
@@ -36,7 +36,7 @@ fn path_plane_click_retains_source_and_previews_then_edits_in_place() {
let built = crate::preview::compute(&h.app.session, &commands, crate::preview::Colors::from_tokens(&crate::theme::Tokens::dark())).unwrap();
assert!(!built.scene.xray.is_empty(), "construction preview must contain visible geometry");
let plane = preview.after.construct.last().unwrap();
let solvecraft_engine::doc::construct::ConstructGeom::Plane(pl) = plane.geom else { panic!("plane") };
let solidcraft_engine::doc::construct::ConstructGeom::Plane(pl) = plane.geom else { panic!("plane") };
assert!(pl.origin.dist(Vec3::new(20.0, 27.5, 25.0)) < 1e-6);
assert_eq!(h.app.session.doc.features.len(), count);
assert_eq!(h.app.session.undo.len(), history);
@@ -54,7 +54,7 @@ fn path_plane_click_retains_source_and_previews_then_edits_in_place() {
h.step(&json!({"key":"Enter"})).unwrap();
assert!(h.app.dialog.is_none(), "{:?}", h.app.dialog);
assert_eq!(h.app.session.doc.features.len(), count + 1);
let p = solvecraft_engine::view::construction_planes(&h.app.session).into_iter().find(|p| p.0 == feature.id).unwrap().2;
let p = solidcraft_engine::view::construction_planes(&h.app.session).into_iter().find(|p| p.0 == feature.id).unwrap().2;
assert!(p.origin.dist(Vec3::new(15.0, 23.75, 17.5)) < 1e-6, "{p:?}");
h.app.run("edit.undo", json!({})).unwrap();
assert_eq!(h.app.session.doc.feature(feature.id).unwrap().kind, feature.kind);
@@ -94,7 +94,7 @@ fn cylinder_emboss_empty_target_face_applies_automatic_wrap_and_deboss() {
h.step(&json!({"key":"Enter"})).unwrap();
assert!(h.app.dialog.is_none(), "{:?}", h.app.dialog);
let feature = h.app.session.doc.features.last().unwrap().clone();
let solvecraft_engine::doc::FeatureKind::Emboss { face: Some(face), deboss: actual, .. } = &feature.kind else {
let solidcraft_engine::doc::FeatureKind::Emboss { face: Some(face), deboss: actual, .. } = &feature.kind else {
panic!("automatic Apply must save its cylinder anchor: {:?}", feature.kind)
};
assert_eq!(*actual, deboss);
@@ -172,7 +172,7 @@ fn cylinder_emboss_edit_retains_second_source_and_author_paths_and_all_profiles(
.as_u64()
.unwrap();
let before = h.app.session.doc.feature(feature).unwrap().clone();
let solvecraft_engine::doc::FeatureKind::Emboss { face: Some(face), .. } = &before.kind else { panic!("selected face") };
let solidcraft_engine::doc::FeatureKind::Emboss { face: Some(face), .. } = &before.kind else { panic!("selected face") };
assert_eq!(face.path, vec![source_second]);
assert_eq!(face.author_path, vec![author_second]);
h.app.run("component.activate", json!({"component":"root"})).unwrap();
@@ -396,7 +396,7 @@ fn hole_at_sketch_points() {
{"command": "sketch.point", "params": {"point": [30, 20], "id": "h2"}},
{"command": "sketch.finish", "params": {}},
]));
let sk = app.session.doc.features.iter().find(|f| matches!(f.kind, solvecraft_engine::doc::FeatureKind::Sketch { .. })).unwrap().id;
let sk = app.session.doc.features.iter().find(|f| matches!(f.kind, solidcraft_engine::doc::FeatureKind::Sketch { .. })).unwrap().id;
app.start("solid.hole");
let mut d = app.dialog.clone().unwrap();
if let Kind::Hole { opts, .. } = &mut d.kind {
+6 -6
View File
@@ -1,5 +1,5 @@
//! Live operation previews. While a feature dialog is open, its commands are evaluated on a
//! scratch copy of the session ([`solvecraft_engine::Session::preview`]), off the UI thread
//! scratch copy of the session ([`solidcraft_engine::Session::preview`]), off the UI thread
//! where threads exist, and the result is drawn in place of the bodies it changes: new surface
//! tinted, removed material as a translucent ghost. The last good preview stays up while the
//! next one computes or when an input makes the feature fail (the error shows in the dialog).
@@ -9,9 +9,9 @@ use std::hash::{Hash, Hasher};
use std::sync::mpsc::{Receiver, TryRecvError};
use serde_json::Value;
use solvecraft_engine::Session;
use solvecraft_engine::doc::ModelState;
use solvecraft_engine::geom::{Aabb3, Mesh, Vec3};
use solidcraft_engine::Session;
use solidcraft_engine::doc::ModelState;
use solidcraft_engine::geom::{Aabb3, Mesh, Vec3};
use crate::SolveApp;
use crate::gpu::{GpuScene, SceneSlot};
@@ -299,7 +299,7 @@ pub fn build(before: &ModelState, after: &ModelState, c: Colors) -> Built {
if before.construct.iter().any(|old| old == construct) {
continue;
}
if let solvecraft_engine::doc::construct::ConstructGeom::Plane(plane) = construct.geom {
if let solidcraft_engine::doc::construct::ConstructGeom::Plane(plane) = construct.geom {
let (o, x, y) = (plane.origin, plane.x * 10.0, plane.y * 10.0);
let quad = [o - x - y, o + x - y, o + x + y, o - x + y];
let color = [c.added[0], c.added[1], c.added[2], 64];
@@ -459,7 +459,7 @@ mod tests {
#[test]
fn a_hole_shows_its_cut_through_the_body() {
let mut s = solvecraft_engine::Session::default();
let mut s = solidcraft_engine::Session::default();
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
let p = s.preview(&[("solid.hole".to_string(), json!({"position": [20, 15, 20], "diameter": 5, "depth": 10}))]).unwrap();
let b = build(&p.before, &p.after, colors());
+1 -1
View File
@@ -2,7 +2,7 @@
use egui::RichText;
use serde_json::json;
use solvecraft_engine::recovery::{self, Autosaver, Entry};
use solidcraft_engine::recovery::{self, Autosaver, Entry};
use crate::SolveApp;
+2 -2
View File
@@ -6,8 +6,8 @@ use std::collections::HashMap;
use std::sync::Arc;
use egui::{Color32, Mesh, Shape, TextureHandle, TextureId, pos2};
use solvecraft_engine::doc::canvas::Canvas;
use solvecraft_engine::geom::Vec2;
use solidcraft_engine::doc::canvas::Canvas;
use solidcraft_engine::geom::Vec2;
use crate::SolveApp;
use crate::gpu::GpuImage;
+12 -12
View File
@@ -1,6 +1,6 @@
use crate::scenario::Harness;
use serde_json::{Value, json};
use solvecraft_engine::doc::{AxisRef, FeatureKind};
use solidcraft_engine::doc::{AxisRef, FeatureKind};
fn make(h: Harness, axis: Value) -> Harness {
make_params(h, axis, json!({}))
@@ -32,12 +32,12 @@ fn unchanged_all_profiles_retains_future_geometry_but_reselection_is_explicit()
}
h.step(&json!({"widget":"OK"})).unwrap();
let FeatureKind::Revolve { profiles, .. } = &h.app.session.doc.find_feature("Turn").unwrap().kind else { panic!("revolve") };
assert_eq!(matches!(profiles, solvecraft_engine::doc::ProfileSel::All), !reselect);
assert_eq!(matches!(profiles, solidcraft_engine::doc::ProfileSel::All), !reselect);
h.app.run("sketch.edit", json!({"sketch":"Profile"})).unwrap();
h.app.run("sketch.rectangle.two_point", json!({"p0":[20,0],"p1":[25,5]})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
let state = h.app.session.world_state();
let volume: f64 = state.bodies.iter().map(|b| solvecraft_engine::doc::kernel::measure(&b.body).unwrap().volume).sum();
let volume: f64 = state.bodies.iter().map(|b| solidcraft_engine::doc::kernel::measure(&b.body).unwrap().volume).sum();
let expected = if reselect { 490.873852 } else { 1374.446786 };
assert!((volume - expected).abs() < 0.3, "{volume} != {expected}");
}
@@ -53,24 +53,24 @@ fn to_object_noop_preserves_cut_targets_and_untargeted_body() {
let state = h.app.session.world_state();
// A quarter annulus separates the stock into inner and outer solids. Both are
// results of cutting A; checking only the first named solid loses material.
let cut_volume = |state: &solvecraft_engine::doc::ModelState| -> f64 {
state.bodies.iter().filter(|b| b.name != "B").map(|b| solvecraft_engine::doc::kernel::measure(&b.body).unwrap().volume).sum()
let cut_volume = |state: &solidcraft_engine::doc::ModelState| -> f64 {
state.bodies.iter().filter(|b| b.name != "B").map(|b| solidcraft_engine::doc::kernel::measure(&b.body).unwrap().volume).sum()
};
let before_a = cut_volume(&state);
let removed = 0.5 * (15.0f64.powi(2) - 10.0f64.powi(2)) * 5.0 * std::f64::consts::FRAC_PI_2;
assert!((before_a - (2000.0 - removed)).abs() < 0.2, "cut results {:?}: {before_a}", state.bodies.iter().map(|b| &b.name).collect::<Vec<_>>());
let in_cut_stock = |p| state.bodies.iter().filter(|b| b.name != "B").any(|b| b.body.contains(p));
assert!(in_cut_stock(solvecraft_engine::geom::Vec3::new(2.0, 2.0, 2.0)));
assert!(in_cut_stock(solvecraft_engine::geom::Vec3::new(18.0, 2.0, 18.0)));
assert!(!in_cut_stock(solvecraft_engine::geom::Vec3::new(9.0, 2.0, 9.0)));
assert!(state.body("B").unwrap().body.contains(solvecraft_engine::geom::Vec3::new(9.0, 2.0, 9.0)));
assert!(in_cut_stock(solidcraft_engine::geom::Vec3::new(2.0, 2.0, 2.0)));
assert!(in_cut_stock(solidcraft_engine::geom::Vec3::new(18.0, 2.0, 18.0)));
assert!(!in_cut_stock(solidcraft_engine::geom::Vec3::new(9.0, 2.0, 9.0)));
assert!(state.body("B").unwrap().body.contains(solidcraft_engine::geom::Vec3::new(9.0, 2.0, 9.0)));
let before_bounds: Vec<_> = state.bodies.iter().map(|b| (b.name.clone(), b.mesh().bounds())).collect();
let undo = h.app.session.undo.len();
open(&mut h);
h.step(&json!({"widget":"OK"})).unwrap();
assert!(h.app.dialog.is_none());
let state = h.app.session.world_state();
assert!((solvecraft_engine::doc::kernel::measure(&state.body("B").unwrap().body).unwrap().volume - 2000.0).abs() < 1e-7);
assert!((solidcraft_engine::doc::kernel::measure(&state.body("B").unwrap().body).unwrap().volume - 2000.0).abs() < 1e-7);
assert!((cut_volume(&state) - before_a).abs() < 1e-7);
assert_eq!(state.bodies.iter().map(|b| (b.name.clone(), b.mesh().bounds())).collect::<Vec<_>>(), before_bounds);
assert_eq!(h.app.session.undo.len(), undo + 1);
@@ -116,7 +116,7 @@ fn second_active_occurrence_noop_preserves_definition_axis_target_and_instances(
h.app.run("component.activate", json!({"component":owner["component"],"occurrence":second,"path":[second]})).unwrap();
let original = serde_json::to_value(&h.app.session.doc.find_feature("Turn").unwrap().kind).unwrap();
let frame = h.app.session.active_component_frame();
let expected_origin = solvecraft_engine::doc::apply_point(&frame, solvecraft_engine::geom::Vec3::ZERO);
let expected_origin = solidcraft_engine::doc::apply_point(&frame, solidcraft_engine::geom::Vec3::ZERO);
let before: Vec<_> = h.app.session.world_state().bodies.iter().map(|b| (b.name.clone(), b.mesh().bounds())).collect();
open(&mut h);
let (origin, _) = crate::canvas::revolve_axis(&h.app, h.app.dialog.as_ref().unwrap()).unwrap();
@@ -154,7 +154,7 @@ fn actual_noop_editor_retains_all_revolve_axis_kinds_and_analytic_volume() {
let bounds = body.mesh().bounds();
let radii = if axis_value == json!("l4") { 25.0 } else { 225.0 - 100.0 };
let expected = 0.5 * radii * 5.0 * std::f64::consts::FRAC_PI_2;
let volume = solvecraft_engine::doc::kernel::measure(&body.body).unwrap().volume;
let volume = solidcraft_engine::doc::kernel::measure(&body.body).unwrap().volume;
assert!((volume - expected).abs() < 0.03, "{axis_value}: {volume}");
open(&mut h);
let commands = crate::dialogs::apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap();
+15 -15
View File
@@ -1,6 +1,6 @@
use crate::scenario::Harness;
use serde_json::{Value, json};
use solvecraft_engine::doc::{FeatureKind, kernel};
use solidcraft_engine::doc::{FeatureKind, kernel};
fn fixture(mut h: Harness, extra: Value) -> Harness {
h.app.run("sketch.create", json!({"plane":"XY","name":"Profile"})).unwrap();
@@ -91,7 +91,7 @@ fn symmetric_boundary_drags_edit_included_angle_and_cancel_is_lossless() {
assert!((volume(&h) - expected_volume(90.0)).abs() < 0.1, "{handle}: {} angle {}", volume(&h), angle(&h));
let bounds = h.app.session.world_state().body("Main").unwrap().mesh().bounds();
assert!((bounds.max.z - 15.0 / 2.0f64.sqrt()).abs() < 0.03 && (bounds.min.z + 15.0 / 2.0f64.sqrt()).abs() < 0.03);
let path = h.dir.join("symmetric.solvecraft");
let path = h.dir.join("symmetric.solidcraft");
h.app.run("file.save", json!({"path":path})).unwrap();
h.app.run("doc.open", json!({"path":path})).unwrap();
assert_eq!(angle(&h), "90 deg");
@@ -154,7 +154,7 @@ fn selected_second_placement_custom_axis_handle_and_preview_stay_in_its_frame()
let frame = h.app.session.active_component_frame();
let world = |degrees: f64| {
let a = degrees.to_radians();
let p = solvecraft_engine::doc::apply_point(&frame, solvecraft_engine::geom::Vec3::new(12.5 * a.cos(), 2.5, 12.5 * a.sin()));
let p = solidcraft_engine::doc::apply_point(&frame, solidcraft_engine::geom::Vec3::new(12.5 * a.cos(), 2.5, 12.5 * a.sin()));
json!([p.x, p.y, p.z])
};
let axis = match &h.app.session.doc.find_feature("Turn").unwrap().kind {
@@ -186,7 +186,7 @@ fn curved_to_object_has_surface_feedback_without_an_angular_handle_or_input() {
h.step(&json!({"click":{"handle":"dialog:Type:To Object"}})).unwrap();
let mut dialog = h.app.dialog.take().unwrap();
dialog.active = dialog.inputs.iter().position(|input| input.label == "To Object").unwrap();
dialog.pick(&h.app.session, solvecraft_engine::Sel::Vertex { body: "Target".into(), point: solvecraft_engine::geom::Vec3::new(0.0, 2.0, 15.0) });
dialog.pick(&h.app.session, solidcraft_engine::Sel::Vertex { body: "Target".into(), point: solidcraft_engine::geom::Vec3::new(0.0, 2.0, 15.0) });
h.app.dialog = Some(dialog);
h.frames(8);
assert!(h.widget("To surface").is_some());
@@ -206,7 +206,7 @@ fn free_drag_and_invalid_expression_stay_editable_before_commit() {
open(&mut h);
h.step(&json!({"drag":[{"handle":"revolve"},{"world":at_angle(37.3)}],"ctrl":true})).unwrap();
let crate::dialogs::Kind::Revolve { angle: entered, .. } = &h.app.dialog.as_ref().unwrap().kind else { panic!("revolve") };
assert!((h.app.session.doc.eval(entered, solvecraft_engine::doc::expr::Kind::Angle).unwrap().to_degrees() - 37.3).abs() < 0.2);
assert!((h.app.session.doc.eval(entered, solidcraft_engine::doc::expr::Kind::Angle).unwrap().to_degrees() - 37.3).abs() < 0.2);
h.step(&json!({"click":{"handle":"dialog:Angle"}})).unwrap();
h.step(&json!({"key":"A","cmd":true})).unwrap();
h.step(&json!({"text":"unknown_angle"})).unwrap();
@@ -257,15 +257,15 @@ fn legacy_reversed_axis_symmetric_interval_is_shown_and_preserved_exactly() {
);
// Exercise an actual older design: no convention marker was stored by version 10.
let mut saved: Value = serde_json::from_str(&h.app.session.doc.to_json()).unwrap();
saved["format"] = json!("solvecraft/10");
saved["format"] = json!("solidcraft/10");
for feature in saved["features"].as_array_mut().unwrap() {
feature.as_object_mut().unwrap().remove("convention");
}
let path = h.dir.join("legacy-revolve.solvecraft");
let path = h.dir.join("legacy-revolve.solidcraft");
std::fs::write(&path, serde_json::to_vec(&saved).unwrap()).unwrap();
h.app.run("doc.open", json!({"path":path})).unwrap();
let FeatureKind::Revolve { convention, .. } = &h.app.session.doc.find_feature("Turn").unwrap().kind else { panic!("revolve") };
assert_eq!(*convention, solvecraft_engine::doc::RevolveConvention::LegacyAxisStart);
assert_eq!(*convention, solidcraft_engine::doc::RevolveConvention::LegacyAxisStart);
let original = serde_json::to_value(&h.app.session.doc.find_feature("Turn").unwrap().kind).unwrap();
let before = h.app.session.world_state().body("Main").unwrap().mesh().bounds();
assert!(before.min.x < -14.9 && before.max.x < 7.6 && before.min.z > -1e-6 && before.max.z > 14.9);
@@ -307,8 +307,8 @@ fn canonical_symmetric_ring_preview_drag_and_reopen_match_material() {
let commands = crate::dialogs::apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap();
let preview = h.app.session.preview_world(&commands).unwrap();
let body = &preview.after.body("Main").unwrap().body;
assert!(body.contains(solvecraft_engine::geom::Vec3::new(3.0, 5.0, 0.0)));
assert!(!body.contains(solvecraft_engine::geom::Vec3::new(0.0, 5.0, 3.0)));
assert!(body.contains(solidcraft_engine::geom::Vec3::new(3.0, 5.0, 0.0)));
assert!(!body.contains(solidcraft_engine::geom::Vec3::new(0.0, 5.0, 3.0)));
let bounds = preview.after.body("Main").unwrap().mesh().bounds();
assert!((bounds.min.x - 2.0f64.sqrt()).abs() < 0.02 && (bounds.max.x - 4.0).abs() < 0.02, "{bounds:?}");
assert!((bounds.min.z + 2.0 * 2.0f64.sqrt()).abs() < 0.02 && (bounds.max.z - 2.0 * 2.0f64.sqrt()).abs() < 0.02);
@@ -320,14 +320,14 @@ fn canonical_symmetric_ring_preview_drag_and_reopen_match_material() {
assert_eq!(angle(&h), "60 deg");
assert_eq!(h.app.session.doc.find_feature("Turn").unwrap().param_names, names);
assert!((volume(&h) - 60.0 * std::f64::consts::FRAC_PI_3).abs() < 0.03);
let path = h.dir.join("canonical-revolve.solvecraft");
let path = h.dir.join("canonical-revolve.solidcraft");
h.app.run("file.save", json!({"path":path})).unwrap();
h.app.run("doc.open", json!({"path":path})).unwrap();
open(&mut h);
handle_at(&mut h, "revolve", at(30.0));
handle_at(&mut h, "revolve2", at(-30.0));
let FeatureKind::Revolve { convention, .. } = &h.app.session.doc.find_feature("Turn").unwrap().kind else { panic!("revolve") };
assert_eq!(*convention, solvecraft_engine::doc::RevolveConvention::SketchNormal);
assert_eq!(*convention, solidcraft_engine::doc::RevolveConvention::SketchNormal);
h.step(&json!({"widget":"Cancel"})).unwrap();
}
}
@@ -347,15 +347,15 @@ fn reversed_axis_to_object_canvas_sweep_and_preview_follow_the_quarter_target()
let preview = h.app.session.preview_world(&commands).unwrap();
let result = preview.after.body("Main").unwrap();
assert_eq!(result.mesh().bounds(), before);
assert!(result.body.contains(solvecraft_engine::geom::Vec3::new(p, 2.5, p)));
assert!(result.body.contains(solidcraft_engine::geom::Vec3::new(p, 2.5, p)));
for (x, z) in [(-p, p), (-p, -p), (p, -p)] {
assert!(!result.body.contains(solvecraft_engine::geom::Vec3::new(x, 2.5, z)));
assert!(!result.body.contains(solidcraft_engine::geom::Vec3::new(x, 2.5, z)));
}
assert!((kernel::measure(&result.body).unwrap().volume - expected_volume(90.0)).abs() < 0.03);
h.step(&json!({"widget":"OK"})).unwrap();
assert_eq!(serde_json::to_value(&h.app.session.doc.find_feature("Turn").unwrap().kind).unwrap(), original);
assert_eq!(h.app.session.doc.find_feature("Turn").unwrap().param_names, names);
let path = h.dir.join("reversed-to-object.solvecraft");
let path = h.dir.join("reversed-to-object.solidcraft");
h.app.run("file.save", json!({"path":path})).unwrap();
h.app.run("doc.open", json!({"path":path})).unwrap();
open(&mut h);
+11 -11
View File
@@ -18,7 +18,7 @@
//! - `{"choose_file": "part.step"}`: the next Open picker returns that file of the scenario's
//! folder (Save pickers save there)
//! - `{"shot": "name"}`: a screenshot when run in a window; headless, a render of the model
//! into `$SOLVECRAFT_SCENARIO_SHOTS` when that is set; `{"debug": 1}`
//! into `$SOLIDCRAFT_SCENARIO_SHOTS` when that is set; `{"debug": 1}`
//! prints the UI state (`"widgets"`: also every widget's text; `"handles"`: every published
//! handle, browser rows as `row:<label>` and their fold arrows as `fold:<label>`)
//! - `{"camera": {"yaw": rad, "pitch": rad}}` puts the camera at an orbit; `{"view_anim": "top",
@@ -41,7 +41,7 @@ use std::path::PathBuf;
use std::rc::Rc;
use serde_json::{Value, json};
use solvecraft_engine::Session;
use solidcraft_engine::Session;
use crate::control::{ControlRequest, Outcome, handle};
use crate::{Services, SolveApp};
@@ -114,7 +114,7 @@ fn with_dir(v: Value, dir: &str) -> Value {
fn scratch_dir() -> PathBuf {
use std::sync::atomic::{AtomicUsize, Ordering};
static N: AtomicUsize = AtomicUsize::new(0);
let d = std::env::temp_dir().join(format!("solvecraft-scenario-{}-{}", std::process::id(), N.fetch_add(1, Ordering::Relaxed)));
let d = std::env::temp_dir().join(format!("solidcraft-scenario-{}-{}", std::process::id(), N.fetch_add(1, Ordering::Relaxed)));
let _ = std::fs::create_dir_all(&d);
d
}
@@ -135,7 +135,7 @@ pub struct Harness {
time: f64,
size: egui::Vec2,
/// The camera as the last `camera` step left it.
cam_mark: Option<solvecraft_engine::render::Camera>,
cam_mark: Option<solidcraft_engine::render::Camera>,
}
impl Default for Harness {
@@ -145,9 +145,9 @@ impl Default for Harness {
}
impl Drop for Harness {
/// The scenario's files go with it (kept with SOLVECRAFT_SCENARIO_KEEP set).
/// The scenario's files go with it (kept with SOLIDCRAFT_SCENARIO_KEEP set).
fn drop(&mut self) {
if std::env::var_os("SOLVECRAFT_SCENARIO_KEEP").is_none() {
if solidcraft_engine::geom::env::var_os("SCENARIO_KEEP").is_none() {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
@@ -324,7 +324,7 @@ impl Harness {
/// frame may turn the view (its right or back vector) by more than `max_deg`, the last
/// included, so the move ends without a jump.
fn view_anim(&mut self, v: &str, max_deg: f64) -> Result<(), String> {
let basis = |c: &solvecraft_engine::render::Camera| c.basis();
let basis = |c: &solidcraft_engine::render::Camera| c.basis();
let mut prev = basis(&self.app.cam);
self.app.animate_view(v);
if self.app.cam_anim.is_none() && self.app.viewport.orbit_snap.is_none() {
@@ -334,7 +334,7 @@ impl Harness {
for i in 0..240 {
self.frame();
let now = basis(&self.app.cam);
let ang = |a: solvecraft_engine::geom::Vec3, b: solvecraft_engine::geom::Vec3| a.dot(b).clamp(-1.0, 1.0).acos().to_degrees();
let ang = |a: solidcraft_engine::geom::Vec3, b: solidcraft_engine::geom::Vec3| a.dot(b).clamp(-1.0, 1.0).acos().to_degrees();
let d = ang(prev.0, now.0).max(ang(prev.2, now.2));
if d > worst.0 {
worst = (d, i);
@@ -403,7 +403,7 @@ impl Harness {
v
} else if let Some(f) = s.get("script").and_then(Value::as_str) {
// A command script (`{"commands": [{command, params}]}`, path from the repository
// root) run through the engine, as `solvecraft-cli run` does.
// root) run through the engine, as `solidcraft-cli run` does.
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").join(f);
let text = std::fs::read_to_string(&path).map_err(|e| format!("{f}: {e}"))?;
let v: Value = serde_json::from_str(&text).map_err(|e| format!("{f}: {e}"))?;
@@ -422,8 +422,8 @@ impl Harness {
} else if s.get("note").is_some() || s.get("pending").is_some() {
return Ok(());
} else if let Some(name) = s.get("shot").and_then(Value::as_str) {
// With SOLVECRAFT_SCENARIO_SHOTS set: a render of the model as the camera sees it.
if let Ok(dir) = std::env::var("SOLVECRAFT_SCENARIO_SHOTS") {
// With SOLIDCRAFT_SCENARIO_SHOTS set: a render of the model as the camera sees it.
if let Ok(dir) = solidcraft_engine::geom::env::var("SCENARIO_SHOTS").ok_or(()) {
let path = std::path::Path::new(&dir).join(format!("{name}.png"));
self.call("ui.render", json!({"path": path.to_string_lossy(), "width": 1000, "height": 700}));
}
+3 -3
View File
@@ -3,8 +3,8 @@
//! selection. The pure parts are unit tested; the viewport uses them.
use egui::{Pos2, Rect};
use solvecraft_engine::Sel;
use solvecraft_engine::geom::Vec3;
use solidcraft_engine::Sel;
use solidcraft_engine::geom::Vec3;
use crate::viewport::Hit;
@@ -180,7 +180,7 @@ fn segments_cross(p: Pos2, q: Pos2, r: Pos2, s: Pos2) -> bool {
mod tests {
use super::*;
use egui::pos2;
use solvecraft_engine::render::{Camera, StandardView};
use solidcraft_engine::render::{Camera, StandardView};
#[test]
fn origin_widget_keeps_its_screen_size() {
@@ -1,6 +1,6 @@
use crate::scenario::Harness;
use serde_json::json;
use solvecraft_engine::{
use solidcraft_engine::{
Sel,
doc::{FaceAttachmentStatus, FeatureKind, PlaneRef},
};
+5 -5
View File
@@ -6,7 +6,7 @@
use egui::{Align2, FontId, Pos2, Rect, Sense, Stroke, pos2, vec2};
use serde_json::{Value, json};
use solvecraft_engine::command_specs;
use solidcraft_engine::command_specs;
use crate::theme::Tokens;
use crate::{SolveApp, icons};
@@ -162,7 +162,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
if !app.sbox.pinned.is_empty() {
ui.horizontal_wrapped(|ui| {
for id in app.sbox.pinned.clone() {
let Some(c) = solvecraft_engine::find_command(&id) else { continue };
let Some(c) = solidcraft_engine::find_command(&id) else { continue };
let (r, resp) = ui.allocate_exact_size(vec2(28.0, 28.0), Sense::click());
if resp.hovered() {
ui.painter().rect_filled(r, 4.0, t.hover);
@@ -189,7 +189,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
egui::ScrollArea::vertical().max_height(h - 90.0).show(ui, |ui| {
ui.spacing_mut().item_spacing.y = 0.0;
for (i, (id, label, icon, place)) in list.iter().enumerate().take(80) {
let enabled = solvecraft_engine::find_command(id).is_some_and(|c| c.info(&app.session).enabled);
let enabled = solidcraft_engine::find_command(id).is_some_and(|c| c.info(&app.session).enabled);
let (r, resp) = ui.allocate_exact_size(vec2(W - 20.0, 26.0), Sense::click());
let lit = i == app.sbox.index || resp.hovered();
if lit {
@@ -280,7 +280,7 @@ mod tests {
#[test]
fn search_finds_by_name_id_and_panel_and_puts_recent_first() {
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
assert_eq!(search(&app, "extr").first().map(|x| x.0), Some("solid.extrude"));
assert_eq!(search(&app, "solid.fil").first().map(|x| x.0), Some("solid.fillet"));
assert!(search(&app, "constraints").iter().any(|x| x.0 == "sketch.constraint.parallel"), "by panel");
@@ -289,7 +289,7 @@ mod tests {
let all = search(&app, "");
assert_eq!((all[0].0, all[1].0), ("solid.revolve", "solid.shell"));
app.sbox.toggle_pin("solid.extrude");
let mut other = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
let mut other = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
other.sbox.load(&app.sbox.prefs());
assert_eq!((other.sbox.pinned.clone(), other.sbox.recent.len()), (vec!["solid.extrude".to_string()], 2));
}
+2 -2
View File
@@ -9,7 +9,7 @@ use std::cell::RefCell;
use egui::{Pos2, Rect, Shape, Stroke, vec2};
use serde_json::{Value, json};
use solvecraft_engine::geom::{Vec2, Vec3};
use solidcraft_engine::geom::{Vec2, Vec3};
use crate::SolveApp;
use crate::theme::Tokens;
@@ -224,7 +224,7 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) -
if let Some((h, _)) = hover {
w.push(h);
}
let poly: Vec<Pos2> = solvecraft_engine::sketch::fit_polyline(&w).into_iter().filter_map(|p| proj.to_screen(p)).collect();
let poly: Vec<Pos2> = solidcraft_engine::sketch::fit_polyline(&w).into_iter().filter_map(|p| proj.to_screen(p)).collect();
painter.add(Shape::line(poly, stroke));
} else {
painter.add(Shape::line(all, stroke));
@@ -1,7 +1,7 @@
//! Real 3D drawing and triad input in a rotated second component occurrence.
use serde_json::json;
use solvecraft_engine::geom::Vec3;
use solidcraft_engine::geom::Vec3;
#[test]
fn bridge_line_snaps_to_points_on_two_visible_sketch_planes() {
@@ -82,7 +82,7 @@ fn sketch_tools_never_pick_another_sketchs_point() {
);
let (at, id) = crate::viewport::sketch_point_at(&h.app, &proj, pos).unwrap();
assert!(id.is_none(), "sketch_3d={sketch_3d}: snapped to foreign point id {id:?}");
assert!(at.dist(solvecraft_engine::geom::Vec2::new(10.0, 20.0)) < 0.5, "{at:?}");
assert!(at.dist(solidcraft_engine::geom::Vec2::new(10.0, 20.0)) < 0.5, "{at:?}");
if !sketch_3d {
let before = h.app.session.world_state().sketch(other).unwrap().sketch.clone();
// A real 2D line click there creates nothing tied to the other sketch's point.
+6 -6
View File
@@ -2,10 +2,10 @@
use egui::{Pos2, RichText};
use serde_json::{Value, json};
use solvecraft_engine::Sel;
use solvecraft_engine::doc::face_anchor::plane_face_anchor;
use solvecraft_engine::doc::{EmbossFace, FaceAttachmentStatus, FeatureKind, mat_inverse};
use solvecraft_engine::geom::{Plane, Vec3};
use solidcraft_engine::Sel;
use solidcraft_engine::doc::face_anchor::plane_face_anchor;
use solidcraft_engine::doc::{EmbossFace, FaceAttachmentStatus, FeatureKind, mat_inverse};
use solidcraft_engine::geom::{Plane, Vec3};
use crate::SolveApp;
use crate::theme::Tokens;
@@ -253,7 +253,7 @@ pub fn panel(app: &mut SolveApp, ctx: &egui::Context, at: Pos2) {
/// Only a lost reference's explicit saved author frame supplies a guide. It is
/// absent from ModelState, selection, snapping and all downstream evaluation.
pub fn lost_guide(app: &SolveApp) -> Option<(Plane, &solvecraft_engine::sketch::Sketch)> {
pub fn lost_guide(app: &SolveApp) -> Option<(Plane, &solidcraft_engine::sketch::Sketch)> {
let sketch = app.tree.redefine?;
let FaceAttachmentStatus::Lost { repair_plane: Some(plane), .. } = attachment(app, sketch) else { return None };
let FeatureKind::Sketch { sketch, .. } = &app.session.doc.feature(sketch)?.kind else { return None };
@@ -270,7 +270,7 @@ pub fn guide_lines(app: &SolveApp) -> Vec<Vec<Vec3>> {
pub fn display_guide_lines(app: &SolveApp, proj: &crate::viewport::Proj) -> Vec<Vec<Vec3>> {
let Some((plane, sketch)) = lost_guide(app) else { return Vec::new() };
let mut budget = solvecraft_engine::render::SamplingBudget::new(65_536, 262_144);
let mut budget = solidcraft_engine::render::SamplingBudget::new(65_536, 262_144);
(0..sketch.curves.len())
.flat_map(|index| crate::sketch_display::curve_with_budget(sketch, index, &plane, proj, app.viewport.pixels_per_point(), &mut budget).parts)
.collect()
@@ -1,6 +1,6 @@
use crate::scenario::Harness;
use serde_json::{Value, json};
use solvecraft_engine::doc::{FaceAttachmentStatus, FeatureKind, PlaneRef};
use solidcraft_engine::doc::{FaceAttachmentStatus, FeatureKind, PlaneRef};
fn picked_sketch() -> (Harness, u64) {
let mut h = Harness::new();
@@ -46,7 +46,7 @@ fn actual_picked_face_and_noop_edit_preserve_exact_anchor_and_dimensions() {
h.settle();
h.app.finish_sketch();
assert_eq!(serde_json::to_value(&h.app.session.doc.feature(sketch).unwrap().kind).unwrap(), before);
let path = h.dir.join("picked-face.solvecraft");
let path = h.dir.join("picked-face.solidcraft");
h.app.run("file.save", json!({"path":path})).unwrap();
h.app.run("doc.open", json!({"path":path})).unwrap();
assert_eq!(serde_json::to_value(&h.app.session.doc.feature(sketch).unwrap().kind).unwrap(), before);
@@ -84,7 +84,7 @@ fn lost_attachment_ghost_rebind_preview_cancel_apply_and_undo_preserve_sketch()
h.app.run("browser.visibility", json!({"items":["ControlBox"],"visible":true})).unwrap();
h.frames(4);
h.step(&json!({"click":{"world":[3,4,30]}})).unwrap();
assert!(matches!(h.app.session.selection.first(), Some(solvecraft_engine::Sel::Face { body, .. }) if body=="ControlBox"));
assert!(matches!(h.app.session.selection.first(), Some(solidcraft_engine::Sel::Face { body, .. }) if body=="ControlBox"));
h.frames(2);
h.step(&json!({"widget":"Reattach selected face"})).unwrap();
let commands = crate::sketch_attachment_ui::commands(&h.app)
@@ -113,7 +113,7 @@ fn lost_attachment_ghost_rebind_preview_cancel_apply_and_undo_preserve_sketch()
let boss = h.app.session.world_state().body("Boss").unwrap().mesh().bounds();
assert!((boss.min.z - 30.0).abs() < 1e-7 && (boss.max.z - 32.0).abs() < 1e-7);
let repaired = h.app.session.doc.to_json();
let path = h.dir.join("rebound-face.solvecraft");
let path = h.dir.join("rebound-face.solidcraft");
h.app.run("file.save", json!({"path":path})).unwrap();
h.app.run("edit.undo", json!({})).unwrap();
assert_eq!(h.app.session.doc.to_json(), before);
@@ -130,7 +130,7 @@ fn attachment_drafts_leave_on_document_replacement_but_survive_failed_open() {
h.frames(2);
h.step(&json!({"widget":"Use XZ plane"})).unwrap();
assert!(crate::sketch_attachment_ui::commands(&h.app).is_some());
assert!(h.app.run("doc.open", json!({"path":h.dir.join("missing.solvecraft")})).is_err());
assert!(h.app.run("doc.open", json!({"path":h.dir.join("missing.solidcraft")})).is_err());
assert!(crate::sketch_attachment_ui::commands(&h.app).is_some());
h.app.run("file.new", json!({})).unwrap();
assert!(h.app.tree.redefine.is_none());
@@ -223,7 +223,7 @@ fn actual_nested_second_placement_face_pick_keeps_target_and_author_paths() {
assert_eq!(h.app.session.active_occurrence_path.as_ref(), Some(&path));
h.app.finish_sketch();
assert_eq!(h.app.session.doc.feature(sketch).unwrap().kind, kind);
let file = h.dir.join("nested-face.solvecraft");
let file = h.dir.join("nested-face.solidcraft");
h.app.run("file.save", json!({"path":file})).unwrap();
h.app.run("doc.open", json!({"path":file})).unwrap();
assert_eq!(h.app.session.doc.feature(sketch).unwrap().kind, kind);
@@ -241,9 +241,9 @@ fn actual_nested_second_placement_face_pick_keeps_target_and_author_paths() {
#[test]
fn nested_anchor_saved_fixed_conversion_preserves_rotated_second_author_plane() {
use solvecraft_engine::doc::{EmbossFace, apply_point, apply_vector};
use solvecraft_engine::geom::Plane;
use solvecraft_engine::geom::Vec3;
use solidcraft_engine::doc::{EmbossFace, apply_point, apply_vector};
use solidcraft_engine::geom::Plane;
use solidcraft_engine::geom::Vec3;
let mut h = Harness::new();
h.app.session.auto_project = false;
let created = h.app.run("component.create", json!({"name":"Author"})).unwrap();
@@ -285,7 +285,7 @@ fn nested_anchor_saved_fixed_conversion_preserves_rotated_second_author_plane()
axis_dir: Vec3::X,
angle: "15 deg".into(),
};
h.app.session = solvecraft_engine::Session::new(document);
h.app.session = solidcraft_engine::Session::new(document);
let raw = h.app.session.doc.sketch(id).unwrap().clone();
let before = h.app.session.doc.to_json();
let undo = h.app.session.undo.len();
+4 -4
View File
@@ -1,5 +1,5 @@
use super::*;
use solvecraft_engine::sketch::CurveKind;
use solidcraft_engine::sketch::CurveKind;
#[test]
fn clicked_periodic_fit_closes_at_start_and_right_click_keeps_natural() {
@@ -19,7 +19,7 @@ fn clicked_periodic_fit_closes_at_start_and_right_click_keeps_natural() {
assert_eq!(sk.curves.len(), 1, "{:?}", h.app.status);
assert!(matches!(sk.curves[0].kind, CurveKind::Spline { periodic: true, .. }));
sk.validate_periodic_splines().unwrap();
assert_eq!(solvecraft_engine::sketch::find_profiles(&sk).len(), 1);
assert_eq!(solidcraft_engine::sketch::find_profiles(&sk).len(), 1);
assert!(h.app.tool.as_ref().unwrap().pts.is_empty());
for point in [[15, 2, 0], [20, 2, 0], [20, 6, 0]] {
h.step(&json!({"click":{"world":point}})).unwrap();
@@ -54,7 +54,7 @@ fn clicked_ellipse_offset_then_closed_offset_preserves_exact_distance_and_undo()
assert_eq!(sk.freeform_offsets.len(), 2, "{:?}", h.app.status);
assert!((sk.freeform_offsets[1].distance + 0.5).abs() < 1e-4);
assert!(sk.freeform_offsets.iter().all(|r| r.certificate == 2));
assert_eq!(solvecraft_engine::sketch::find_profiles(&sk).len(), 3);
assert_eq!(solidcraft_engine::sketch::find_profiles(&sk).len(), 3);
let last = sk.curve_index(&sk.freeform_offsets[1].output).unwrap();
assert!(sk.exact_segs(last).unwrap()[0].point_at(0.0).dist(Vec2::new(11.5, 0.0)) < 1e-4);
h.app.run("edit.undo", json!({})).unwrap();
@@ -86,7 +86,7 @@ fn derived_offset_context_action_releases_geometry_and_undo_restores() {
h.app.run("sketch.ellipse", json!({"center":[0,0],"major":[10,0],"minor_radius":5})).unwrap();
let out = h.app.run("sketch.offset", json!({"curves":["e1"],"distance":-1,"chain":false})).unwrap();
let id = out["curves"][0].as_str().unwrap().to_string();
h.app.session.selection = vec![solvecraft_engine::Sel::SketchCurve { id: id.clone() }];
h.app.session.selection = vec![solidcraft_engine::Sel::SketchCurve { id: id.clone() }];
let items = crate::context_menu::items(&h.app, &crate::context_menu::Target::Viewport);
let item = items.iter().find(|item| item.id == "sketch.offset.break").unwrap();
assert_eq!(item.params, Some(json!({"curve":id})));
+2 -2
View File
@@ -6,8 +6,8 @@
use egui::{Pos2, vec2};
use serde_json::{Value, json};
use solvecraft_engine::doc::expr::Kind as ValueKind;
use solvecraft_engine::geom::Vec2;
use solidcraft_engine::doc::expr::Kind as ValueKind;
use solidcraft_engine::geom::Vec2;
use crate::SolveApp;
use crate::theme::Tokens;
+4 -4
View File
@@ -3,10 +3,10 @@ use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use solvecraft_engine::doc::ModelState;
use solvecraft_engine::geom::{Loop2, Plane, Region2, Vec2, Vec3};
use solvecraft_engine::render::{PixelCurveView, SampledPath, SamplingBudget, SamplingQuality, sample_segment, sample_segment_unclipped};
use solvecraft_engine::sketch::Sketch;
use solidcraft_engine::doc::ModelState;
use solidcraft_engine::geom::{Loop2, Plane, Region2, Vec2, Vec3};
use solidcraft_engine::render::{PixelCurveView, SampledPath, SamplingBudget, SamplingQuality, sample_segment, sample_segment_unclipped};
use solidcraft_engine::sketch::Sketch;
use crate::SolveApp;
use crate::viewport::Proj;
@@ -2,16 +2,16 @@ use super::{Tool, preview_curve};
use crate::scenario::Harness;
use crate::viewport::Proj;
use serde_json::{Value, json};
use solvecraft_engine::geom::{Plane, Vec2, Vec3};
use solvecraft_engine::render::{Camera, Mat4};
use solvecraft_engine::sketch::Sketch;
use solidcraft_engine::geom::{Plane, Vec2, Vec3};
use solidcraft_engine::render::{Camera, Mat4};
use solidcraft_engine::sketch::Sketch;
fn tool(command: &str, points: &[[f64; 2]]) -> Tool {
let mut tool = Tool::for_command(command).unwrap();
tool.pts = points.iter().map(|p| (Vec2::new(p[0], p[1]), None)).collect();
tool
}
fn spans(sk: &Sketch) -> Vec<solvecraft_engine::geom::Seg2> {
fn spans(sk: &Sketch) -> Vec<solidcraft_engine::geom::Seg2> {
sk.exact_segs(0).unwrap_or_else(|| sk.segs(0))
}
fn projection(scale: f64) -> Proj {
+10 -10
View File
@@ -3,8 +3,8 @@ use crate::scenario::Harness;
use crate::sketch_display::{curve_sample, projection_key};
use crate::viewport::{Hit, Proj, pick, projection};
use serde_json::json;
use solvecraft_engine::geom::{Plane, Vec2, Vec3};
use solvecraft_engine::render::{SampledPath, SamplingQuality};
use solidcraft_engine::geom::{Plane, Vec2, Vec3};
use solidcraft_engine::render::{SampledPath, SamplingQuality};
use std::sync::Arc;
fn circle(h: &mut Harness) -> u64 {
@@ -78,7 +78,7 @@ fn pixel_circle_fit_exact_half_chord_hover_click_and_model_invariance() {
h.app.viewport.hover
);
h.step(&json!({"click":{"world":[world.x,world.y,world.z]}})).unwrap();
assert!(h.app.session.selection.iter().any(|s| matches!(s, solvecraft_engine::Sel::SketchCurve{id} if id == "c1")));
assert!(h.app.session.selection.iter().any(|s| matches!(s, solidcraft_engine::Sel::SketchCurve{id} if id == "c1")));
h.app.session.selection.clear();
}
assert_eq!(h.app.session.doc.to_json(), baseline, "display and selection must not alter stored geometry, constraints or expressions");
@@ -106,16 +106,16 @@ fn pixel_cache_full_view_resize_and_hidpi_invalidate_within_same_grid_bucket() {
let keys = crate::viewport::display_scene_keys(&h.app, &first);
let a = curve_sample(&h.app, sid, index, &plane, &first);
assert!(Arc::ptr_eq(&a, &curve_sample(&h.app, sid, index, &plane, &first)));
let grid = solvecraft_engine::view::grid_step(h.app.cam.half_height());
let grid = solidcraft_engine::view::grid_step(h.app.cam.half_height());
// Find a real zoom change which keeps the complete grid bucket unchanged.
let distance = h.app.cam.distance;
for factor in [0.8, 0.9, 1.1, 1.2] {
h.app.cam.distance = distance * factor;
if solvecraft_engine::view::grid_step(h.app.cam.half_height()) == grid {
if solidcraft_engine::view::grid_step(h.app.cam.half_height()) == grid {
break;
}
}
assert_eq!(solvecraft_engine::view::grid_step(h.app.cam.half_height()), grid);
assert_eq!(solidcraft_engine::view::grid_step(h.app.cam.half_height()), grid);
assert_ne!(h.app.cam.distance, distance);
let zoom = projection(&h.app, first.rect);
let zoom_keys = crate::viewport::display_scene_keys(&h.app, &zoom);
@@ -163,7 +163,7 @@ fn pixel_adapter_uses_exact_ellipse_periodic_fit_and_control_spline_spans() {
let ss = state.sketch(sid).unwrap();
let exact = ss.sketch.exact_segs(0).unwrap();
assert!(
exact.iter().all(|s| !matches!(s, solvecraft_engine::geom::Seg2::Line { .. })),
exact.iter().all(|s| !matches!(s, solidcraft_engine::geom::Seg2::Line { .. })),
"freeform display must reach the exact span extractor"
);
let proj = projection(&h.app, h.app.viewport.rect.unwrap());
@@ -216,7 +216,7 @@ fn pixel_curve_second_nested_rotated_occurrence_uses_its_world_plane() {
assert!(dense_circle_error(&path, &ss.plane, &proj, 1.0, h.app.viewport.pixels_per_point()) <= 1.0);
let world = ss.plane.to_world(Vec2::new(0.8090169943749475, 0.5877852522924731));
h.step(&json!({"click":{"world":[world.x,world.y,world.z]}})).unwrap();
assert!(h.app.session.selection.iter().any(|s| matches!(s, solvecraft_engine::Sel::SketchCurve{id} if id == "c1")));
assert!(h.app.session.selection.iter().any(|s| matches!(s, solidcraft_engine::Sel::SketchCurve{id} if id == "c1")));
assert_eq!(h.app.session.doc.to_json(), baseline);
}
@@ -231,7 +231,7 @@ fn pixel_annulus_fill_keeps_hole_and_cpu_bridge_matches_cached_boundaries() {
let ss = state.sketch(sid).unwrap();
let proj = projection(&h.app, h.app.viewport.rect.unwrap());
let ppp = h.app.viewport.pixels_per_point();
let profiles = solvecraft_engine::sketch::find_profiles(&ss.sketch);
let profiles = solidcraft_engine::sketch::find_profiles(&ss.sketch);
let ring = profiles.iter().find(|p| p.region.holes.len() == 1).unwrap();
let display = crate::sketch_display::display_region(&ring.region, &ss.plane, &proj, ppp).unwrap();
assert_eq!(display.holes.len(), 1);
@@ -254,7 +254,7 @@ fn pixel_annulus_fill_keeps_hole_and_cpu_bridge_matches_cached_boundaries() {
}
}
assert!((area - std::f64::consts::PI * 0.75).abs() < 0.02, "adaptive fill area: {area}");
let cpu = solvecraft_engine::view::sketch_lines_with_sampler(&ss.sketch, &ss.plane, true, &[], |sk, index, plane| {
let cpu = solidcraft_engine::view::sketch_lines_with_sampler(&ss.sketch, &ss.plane, true, &[], |sk, index, plane| {
crate::sketch_display::curve_parts(sk, index, plane, &proj, ppp)
});
let cached: Vec<_> = (0..ss.sketch.curves.len()).flat_map(|index| curve_sample(&h.app, sid, index, &ss.plane, &proj).parts.clone()).collect();
+1 -1
View File
@@ -2,7 +2,7 @@
//! brings the model up to date in the drag's one undo step, and the grab stays on the geometry.
use serde_json::{Value, json};
use solvecraft_engine::geom::Vec2;
use solidcraft_engine::geom::Vec2;
use crate::scenario::Harness;
+3 -3
View File
@@ -1,12 +1,12 @@
//! Regression tests: editing a sketch from the Browser or timeline can always be left again.
use crate::{Services, SolveApp};
use solvecraft_engine::Session;
use solvecraft_engine::doc::FeatureKind;
use solidcraft_engine::Session;
use solidcraft_engine::doc::FeatureKind;
fn sample_app() -> SolveApp {
let mut s = Session::default();
s.run_script(&solvecraft_engine::sample::script()).unwrap();
s.run_script(&solidcraft_engine::sample::script()).unwrap();
SolveApp::new(s, Services::default())
}
+4 -4
View File
@@ -12,7 +12,7 @@ use std::cell::Cell;
use egui::{Color32, RichText, Stroke, vec2};
use serde_json::{Value, json};
use solvecraft_engine::Sel;
use solidcraft_engine::Sel;
use crate::SolveApp;
use crate::theme::Tokens;
@@ -88,7 +88,7 @@ pub fn toggle_construction(app: &mut SolveApp) {
/// Curves a drawing command just made take the drawing line type.
pub fn apply_mode(app: &mut SolveApp, id: &str, result: &Value) {
let m = mode(app);
if m == 0 || !solvecraft_engine::find_command(id).is_some_and(|c| c.tab == "SKETCH" && c.panel == "CREATE") {
if m == 0 || !solidcraft_engine::find_command(id).is_some_and(|c| c.tab == "SKETCH" && c.panel == "CREATE") {
return;
}
let curves: Vec<String> = result["curves"].as_array().into_iter().flatten().filter_map(|c| c.as_str().map(str::to_string)).collect();
@@ -104,7 +104,7 @@ pub fn apply_mode(app: &mut SolveApp, id: &str, result: &Value) {
ss.sketch
.curve_index(c)
.and_then(|i| ss.sketch.curves.get(i))
.is_some_and(|c| matches!(c.kind, solvecraft_engine::sketch::CurveKind::Line { .. }))
.is_some_and(|c| matches!(c.kind, solidcraft_engine::sketch::CurveKind::Line { .. }))
})
.cloned()
.collect()
@@ -144,7 +144,7 @@ pub fn set_linetype(app: &mut SolveApp, kind: u8) {
ss.sketch
.curve_index(id)
.and_then(|i| ss.sketch.curves.get(i))
.is_some_and(|c| matches!(c.kind, solvecraft_engine::sketch::CurveKind::Line { .. }))
.is_some_and(|c| matches!(c.kind, solidcraft_engine::sketch::CurveKind::Line { .. }))
})
.cloned()
.collect()
+4 -4
View File
@@ -1,7 +1,7 @@
//! Edit Text: the shared feature dialog (OK/Cancel, expression fields) for the source of a
//! sketch text's outlines; OK runs `sketch.edit_text`.
use serde_json::{Value, json};
use solvecraft_engine::{
use solidcraft_engine::{
Sel,
doc::expr::Kind as ValueKind,
sketch::{LinkKind, LinkSource},
@@ -244,14 +244,14 @@ mod tests {
let sk = h.app.session.doc.sketch(sid).unwrap();
let expected = LinkSource::Text {
text: "O".into(),
at: solvecraft_engine::geom::Vec2::new(25.0, 12.0),
at: solidcraft_engine::geom::Vec2::new(25.0, 12.0),
height: 20.0,
angle: std::f64::consts::FRAC_PI_2,
};
assert_eq!(sk.link(&link).unwrap().source, expected);
assert!(!h.app.session.world_state().sketch(sid).unwrap().profiles.is_empty());
let changed = h.app.session.doc.to_json();
let reopened = solvecraft_engine::Session::new(solvecraft_engine::doc::Document::from_json(&changed).unwrap());
let reopened = solidcraft_engine::Session::new(solidcraft_engine::doc::Document::from_json(&changed).unwrap());
assert_eq!(reopened.doc.sketch(sid).unwrap().link(&link).unwrap().source, expected);
h.app.run("edit.undo", json!({})).unwrap();
assert_eq!(h.app.session.doc.to_json(), before);
@@ -322,7 +322,7 @@ mod tests {
assert!(!editing(&h.app));
h.app.start("sketch.edit_text");
assert!(editing(&h.app));
crate::documents::open_tab(&mut h.app, solvecraft_engine::Session::default()).unwrap();
crate::documents::open_tab(&mut h.app, solidcraft_engine::Session::default()).unwrap();
assert!(!editing(&h.app));
}
+21 -21
View File
@@ -7,7 +7,7 @@ use std::cell::{Cell, RefCell};
use egui::{Pos2, Stroke};
use serde_json::{Value, json};
use solvecraft_engine::geom::{Vec2, Vec3};
use solidcraft_engine::geom::{Vec2, Vec3};
use crate::SolveApp;
use crate::tools::{Kind, Tool};
@@ -55,9 +55,9 @@ const ALSO_IN: &[(&str, &str, &str)] = &[
pub fn also_in<'a>(
tab: &str,
panel: &str,
mut cmds: Vec<&'a solvecraft_engine::CommandSpec>,
specs: &[&'a solvecraft_engine::CommandSpec],
) -> Vec<&'a solvecraft_engine::CommandSpec> {
mut cmds: Vec<&'a solidcraft_engine::CommandSpec>,
specs: &[&'a solidcraft_engine::CommandSpec],
) -> Vec<&'a solidcraft_engine::CommandSpec> {
for (t, p, id) in ALSO_IN {
if *t == tab
&& *p == panel
@@ -132,7 +132,7 @@ pub fn start_hook(app: &mut SolveApp, id: &str) -> bool {
return false;
}
// Surface analyses toggle: a second click turns the same one off.
use solvecraft_engine::SurfaceAnalysis as A;
use solidcraft_engine::SurfaceAnalysis as A;
let same = match (id, app.session.analysis) {
("inspect.zebra", Some(A::Zebra { .. })) => Some(true),
("inspect.draft", Some(A::Draft { .. })) => Some(true),
@@ -201,8 +201,8 @@ fn combs(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
pub fn refine_snap(
app: &SolveApp,
proj: &Proj,
sk: &solvecraft_engine::sketch::Sketch,
plane: &solvecraft_engine::geom::Plane,
sk: &solidcraft_engine::sketch::Sketch,
plane: &solidcraft_engine::geom::Plane,
lp: Vec2,
) -> Option<(Vec2, Option<String>)> {
crate::inference::clear();
@@ -220,8 +220,8 @@ pub fn refine_snap(
let mut best: Option<(f64, &'static str, Vec2)> = None;
let mut mid_of: Option<String> = None;
for (i, c) in sk.curves.iter().enumerate() {
if let solvecraft_engine::sketch::CurveKind::Line { .. } = c.kind
&& let Some(solvecraft_engine::sketch::Shape::Line { a, b }) = sk.shape(i)
if let solidcraft_engine::sketch::CurveKind::Line { .. } = c.kind
&& let Some(solidcraft_engine::sketch::Shape::Line { a, b }) = sk.shape(i)
{
let m = (a + b) * 0.5;
let d = m.dist(lp);
@@ -267,7 +267,7 @@ pub fn refine_snap(
{
let (dx, dy) = ((lp.x - prev.x).abs(), (lp.y - prev.y).abs());
// The free coordinate still follows the grid.
let step = solvecraft_engine::view::grid_step(app.cam.half_height()).0 / 10.0;
let step = solidcraft_engine::view::grid_step(app.cam.half_height()).0 / 10.0;
let g = |v: f64| (v / step).round() * step;
if dy < tol && dx > tol {
best = Some((dy, "H", Vec2::new(g(lp.x), prev.y)));
@@ -372,7 +372,7 @@ fn mode(id: &str) -> Option<Mode> {
/// A tool for one of the extra sketch commands.
pub fn tool_for(id: &str) -> Option<Tool> {
mode(id)?;
let spec = solvecraft_engine::find_command(id)?;
let spec = solidcraft_engine::find_command(id)?;
Some(Tool {
cmd: spec.id,
kind: Kind::Ext,
@@ -490,8 +490,8 @@ pub(super) fn corner_point(app: &SolveApp, proj: &Proj, pos: Pos2, cmd: &str) ->
.collect();
if curves.len() != 2
|| curves.iter().any(|(_, c)| {
!matches!(c.kind, solvecraft_engine::sketch::CurveKind::Line { .. })
&& (cmd != "sketch.fillet" || !matches!(c.kind, solvecraft_engine::sketch::CurveKind::Arc { .. }))
!matches!(c.kind, solidcraft_engine::sketch::CurveKind::Line { .. })
&& (cmd != "sketch.fillet" || !matches!(c.kind, solidcraft_engine::sketch::CurveKind::Arc { .. }))
})
{
return None;
@@ -529,7 +529,7 @@ fn corner_len(app: &SolveApp, point: &str) -> f64 {
sk.curves
.iter()
.enumerate()
.filter(|(_, c)| c.kind.uses(q) && matches!(c.kind, solvecraft_engine::sketch::CurveKind::Line { .. }))
.filter(|(_, c)| c.kind.uses(q) && matches!(c.kind, solidcraft_engine::sketch::CurveKind::Line { .. }))
.map(|(i, _)| sk.polyline(i).windows(2).map(|w| w[0].dist(w[1])).sum::<f64>())
.fold(f64::INFINITY, f64::min)
.min(1e6)
@@ -988,7 +988,7 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
crate::tools::paint_preview_curve(&curve, &ss.plane, painter, proj, stroke);
}
// Picked curves highlighted.
let mut display_budget = solvecraft_engine::render::SamplingBudget::new(32_768, 65_536);
let mut display_budget = solidcraft_engine::render::SamplingBudget::new(32_768, 65_536);
for id in t.picks.iter().filter(|x| *x != "|") {
if let Some(ci) = ss.sketch.curve_index(id) {
crate::tools::paint_curve(&ss.sketch, ci, &ss.plane, painter, proj, Stroke::new(3.0, tk.accent.gamma_multiply(0.7)), &mut display_budget);
@@ -1060,7 +1060,7 @@ fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj, hover: Option<Po
&& let Some(c) = ss.sketch.constraints.iter().find(|c| &c.id == id)
{
let (cs, ps) = constraint_entities(&c.kind);
let mut display_budget = solvecraft_engine::render::SamplingBudget::new(32_768, 65_536);
let mut display_budget = solidcraft_engine::render::SamplingBudget::new(32_768, 65_536);
for ci in cs {
crate::tools::paint_curve(&ss.sketch, ci, &ss.plane, painter, proj, Stroke::new(3.0, tk.accent.gamma_multiply(0.7)), &mut display_budget);
}
@@ -1075,7 +1075,7 @@ fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj, hover: Option<Po
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));
|| app.session.selection.iter().any(|s| matches!(s, solidcraft_engine::Sel::SketchConstraint { id: x } if x == id));
let lit = selected || hovered.as_deref() == Some(id.as_str());
let col = if failing {
tk.error
@@ -1130,8 +1130,8 @@ fn glyph_icon(name: &str) -> Option<&'static str> {
}
/// 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::*;
fn constraint_entities(k: &solidcraft_engine::sketch::ConstraintKind) -> (Vec<usize>, Vec<usize>) {
use solidcraft_engine::sketch::ConstraintKind::*;
match *k {
Coincident { p, q } | HorizontalPoints { p, q } | VerticalPoints { p, q } => (vec![], vec![p, q]),
Fix { p } => (vec![], vec![p]),
@@ -1160,8 +1160,8 @@ fn glyph_letter(name: &str) -> &'static str {
/// 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::*;
fn glyph_anchors(sk: &solidcraft_engine::sketch::Sketch, k: &solidcraft_engine::sketch::ConstraintKind) -> Vec<(Vec2, Option<Vec2>)> {
use solidcraft_engine::sketch::ConstraintKind::*;
// Half way along the curve, and the direction there.
let mid = |c: usize| -> Option<(Vec2, Option<Vec2>)> {
let poly = sk.polyline(c);
@@ -1,7 +1,7 @@
//! Keyboard-authored intent must survive stage transitions and later model edits.
use crate::scenario::Harness;
use serde_json::json;
use solvecraft_engine::sketch::{ConstraintKind, CurveKind};
use solidcraft_engine::sketch::{ConstraintKind, CurveKind};
fn start(command: &str) -> Harness {
let mut h = Harness::new();
@@ -39,7 +39,7 @@ fn typed_arc_radius_and_signed_sweep_survive_stages_parameter_edit_and_reopen()
assert!(kinds.iter().any(|k| matches!(k, ConstraintKind::Radius{value,..} if (*value-5.0).abs()<1e-7)));
assert!(kinds.iter().any(|k| matches!(k, ConstraintKind::ArcSweep{value,..} if (*value+std::f64::consts::FRAC_PI_2).abs()<1e-7)));
h.app.run("parameters.change", json!({"name":"arc_radius","expression":"8 mm"})).unwrap();
let path = h.dir.join("typed-arc.solvecraft");
let path = h.dir.join("typed-arc.solidcraft");
h.app.run("file.save", json!({"path":path})).unwrap();
h.app.run("doc.open", json!({"path":path})).unwrap();
let state = h.app.session.world_state();
+3 -3
View File
@@ -1,5 +1,5 @@
//! Standing up a Y-up file (#3). Many CAD programs save parts with Y up, so a STEP or IGES file
//! from them opens lying on its back in a Z-up design. After such a file opens, SolveCraft
//! from them opens lying on its back in a Z-up design. After such a file opens, SolidCraft
//! offers to stand it up: a quarter turn about X (Y → Z) of the bodies it brought, as one
//! Move feature that can be undone or edited. Preferences › Design › "Offer to stand up Y-up
//! files" turns the offer off; with Y up as the default orientation it isn't needed.
@@ -11,8 +11,8 @@ use crate::SolveApp;
/// The bodies a just-opened file brought (`before`: body names before it opened, none for a
/// new design): offer to stand them up.
pub fn offer(app: &mut SolveApp, path: &str, before: &[String]) {
let io = &solvecraft_engine::io::is_step_path;
if !(io(path) || solvecraft_engine::io::is_iges_path(path)) || !app.preferences.offer_stand_up || app.preferences.up_axis != "z" {
let io = &solidcraft_engine::io::is_step_path;
if !(io(path) || solidcraft_engine::io::is_iges_path(path)) || !app.preferences.offer_stand_up || app.preferences.up_axis != "z" {
return;
}
let bodies: Vec<String> = app.session.world_state().bodies.iter().map(|b| b.name.clone()).filter(|n| !before.contains(n)).collect();
+1 -1
View File
@@ -255,7 +255,7 @@ impl Tokens {
/// How a physical material looks (the default appearance follows the material).
pub fn material_color(name: &str) -> Option<Color32> {
solvecraft_engine::doc::material_color(name).map(|[r, g, b]| Color32::from_rgb(r, g, b))
solidcraft_engine::doc::material_color(name).map(|[r, g, b]| Color32::from_rgb(r, g, b))
}
/// Switch themes (the visuals follow on the next [`apply`]).
+4 -4
View File
@@ -2,7 +2,7 @@
use egui::{Color32, Rect, Sense, Stroke, UiBuilder, pos2, vec2};
use serde_json::json;
use solvecraft_engine::doc::FeatureKind;
use solidcraft_engine::doc::FeatureKind;
use crate::theme::Tokens;
use crate::{SolveApp, icons};
@@ -19,7 +19,7 @@ fn icon_of(k: &FeatureKind) -> &'static str {
FeatureKind::Sphere { .. } => "sphere",
FeatureKind::Torus { .. } => "torus",
FeatureKind::Combine { .. } => "combine",
FeatureKind::Pattern { pattern: solvecraft_engine::doc::PatternKind::Rectangular { .. }, .. } => "pattern_rect",
FeatureKind::Pattern { pattern: solidcraft_engine::doc::PatternKind::Rectangular { .. }, .. } => "pattern_rect",
FeatureKind::Pattern { .. } => "pattern_circ",
FeatureKind::Mirror { .. } => "mirror",
FeatureKind::ConstructionPlane { .. } => "plane",
@@ -148,7 +148,7 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
let err = res.and_then(|r| r.error.clone());
let warn = res.and_then(|r| r.warning.clone());
let rolled = i >= marker;
let is_it = |s: &solvecraft_engine::Sel| matches!(s, solvecraft_engine::Sel::Feature { id: f } if f == id);
let is_it = |s: &solidcraft_engine::Sel| matches!(s, solidcraft_engine::Sel::Feature { id: f } if f == id);
let sel = app.session.selection.iter().any(is_it) || app.dialog.as_ref().is_some_and(|d| d.items().iter().any(is_it));
if resp.hovered() {
hovered = Some(*id);
@@ -212,7 +212,7 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
let takes = app.dialog.as_ref().and_then(|d| d.active_input()).is_some_and(|i| i.accept & crate::selection::FEATURES != 0);
if takes {
if let Some(mut d) = app.dialog.take() {
d.pick(&app.session, solvecraft_engine::Sel::Feature { id: *id });
d.pick(&app.session, solidcraft_engine::Sel::Feature { id: *id });
app.dialog = Some(d);
}
} else {
+1 -1
View File
@@ -190,7 +190,7 @@ pub fn resize_zones(ui: &mut Ui) {
mod tests {
use super::*;
use egui::{Event, Pos2, RawInput, ViewportId};
use solvecraft_engine::Session;
use solidcraft_engine::Session;
/// A headless window `width` pt wide showing the application bar (as the title bar) and the
/// resize zones.
+11 -11
View File
@@ -97,9 +97,9 @@ pub fn fit(widths: &[f32], room: f32) -> Vec<Fit> {
/// The commands a panel's drop-down (or its » submenu) lists: its own, then any button moved
/// into it from another panel.
fn menu_commands(
cmds: &[&'static solvecraft_engine::CommandSpec],
shown: &[&'static solvecraft_engine::CommandSpec],
) -> Vec<&'static solvecraft_engine::CommandSpec> {
cmds: &[&'static solidcraft_engine::CommandSpec],
shown: &[&'static solidcraft_engine::CommandSpec],
) -> Vec<&'static solidcraft_engine::CommandSpec> {
let mut out = cmds.to_vec();
for c in shown {
if !out.iter().any(|o| o.id == c.id) {
@@ -340,7 +340,7 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
}
ui.menu_button(app.tr("ui.open_recent"), |ui| {
for path in app.home.recent.clone() {
let missing = !solvecraft_engine::io::vfs::exists(&path);
let missing = !solidcraft_engine::io::vfs::exists(&path);
let label = format!("{path}{}", if missing { " (missing)" } else { "" });
if ui.button(label).clicked() {
crate::home::open_recent(app, &path);
@@ -430,8 +430,8 @@ pub fn save(app: &mut SolveApp) {
}
fn save_as(app: &mut SolveApp) {
let name = format!("{}.{}", app.session.doc.name, solvecraft_engine::io::DESIGN_EXT);
if let Some(p) = app.services.pick_save.as_ref().and_then(|f| f(&name, &[solvecraft_engine::io::DESIGN_EXT])) {
let name = format!("{}.{}", app.session.doc.name, solidcraft_engine::io::DESIGN_EXT);
if let Some(p) = app.services.pick_save.as_ref().and_then(|f| f(&name, &[solidcraft_engine::io::DESIGN_EXT])) {
let _ = app.run("file.save_as", json!({"path": p}));
}
}
@@ -802,7 +802,7 @@ fn dropdown_row_id(id: &str) -> egui::Id {
}
/// How wide a panel drop-down's rows are: the widest label and shortcut, at least 200.
fn dropdown_width(app: &SolveApp, ui: &egui::Ui, cmds: &[&'static solvecraft_engine::CommandSpec]) -> f32 {
fn dropdown_width(app: &SolveApp, ui: &egui::Ui, cmds: &[&'static solidcraft_engine::CommandSpec]) -> f32 {
let t = Tokens::get();
let font = FontId::proportional(13.0);
cmds.iter()
@@ -816,7 +816,7 @@ fn dropdown_width(app: &SolveApp, ui: &egui::Ui, cmds: &[&'static solvecraft_eng
/// One command row of a panel drop-down: icon, label and shortcut, with the hover background
/// under the pointer or keyboard focus (like every menu).
fn dropdown_row(app: &SolveApp, ui: &mut egui::Ui, c: &'static solvecraft_engine::CommandSpec, width: f32, pinned: bool) -> RowAction {
fn dropdown_row(app: &SolveApp, ui: &mut egui::Ui, c: &'static solidcraft_engine::CommandSpec, width: f32, pinned: bool) -> RowAction {
let t = Tokens::get();
let info = c.info(&app.session);
let (rect, _) = ui.allocate_exact_size(vec2(width, ROW_H), Sense::hover());
@@ -886,7 +886,7 @@ struct ToolbarDrag {
/// The dragged command's icon under the pointer, crossed out when releasing removes it.
fn drag_ghost(ctx: &egui::Context, id: &str, at: egui::Pos2, removing: bool) {
let t = Tokens::get();
let Some(c) = solvecraft_engine::find_command(id) else { return };
let Some(c) = solidcraft_engine::find_command(id) else { return };
let p = ctx.layer_painter(egui::LayerId::new(egui::Order::Tooltip, egui::Id::new("sc_toolbar_drag")));
let r = Rect::from_center_size(at + vec2(14.0, 16.0), vec2(30.0, 30.0));
p.rect(r, 4.0, t.toolbar.gamma_multiply(0.92), Stroke::new(1.0, if removing { t.error } else { t.accent }), egui::StrokeKind::Inside);
@@ -1115,7 +1115,7 @@ mod header_report_tests {
h.app.run("sketch.line", json!({"points":[[0,0],[20,0]]})).unwrap();
h.call("ui.view", json!({"view":"top"}));
h.app.cam.distance = 80.0;
h.app.cam.target = solvecraft_engine::geom::Vec3::new(10.0, 0.0, 0.0);
h.app.cam.target = solidcraft_engine::geom::Vec3::new(10.0, 0.0, 0.0);
h.settle();
h.step(&json!({"start":"sketch.dimension"})).unwrap();
for point in [[0, 0, 0], [20, 0, 0], [10, 5, 0]] {
@@ -1158,7 +1158,7 @@ mod home_tests {
h.app.run("solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "Main"})).unwrap();
h.app.run("solid.box", json!({"length": 5, "width": 5, "height": 5, "corner": [20, 0, 0]})).unwrap();
h.app.run("edit.undo", json!({})).unwrap();
h.app.session.selection = vec![solvecraft_engine::Sel::Body { name: "Main".into() }];
h.app.session.selection = vec![solidcraft_engine::Sel::Body { name: "Main".into() }];
let rev = h.app.session.revision;
h.app.home.show(rev);
h.frames(2);
+6 -5
View File
@@ -11,7 +11,7 @@
use std::collections::BTreeMap;
use serde_json::{Value, json};
use solvecraft_engine::{CommandSpec, command_specs};
use solidcraft_engine::{CommandSpec, command_specs};
use crate::SolveApp;
use crate::context_menu::Item;
@@ -55,7 +55,7 @@ fn key(tab: &str, panel: &str) -> String {
/// The buttons a panel shows: the user's list, or the first `promoted` commands of the panel.
pub fn buttons(app: &SolveApp, tab: &str, panel: &str, cmds: &[&'static CommandSpec], promoted: usize) -> Vec<&'static CommandSpec> {
match app.toolbar_custom.panels.get(&key(tab, panel)) {
Some(ids) => ids.iter().filter_map(|id| solvecraft_engine::find_command(id)).collect(),
Some(ids) => ids.iter().filter_map(|id| solidcraft_engine::find_command(id)).collect(),
None => cmds.iter().take(promoted).copied().collect(),
}
}
@@ -101,7 +101,7 @@ fn home_panel(tab: &str, id: &str) -> Option<&'static str> {
/// Pin a command to the toolbar of a tab (as a button of its panel, at the end).
pub fn pin(app: &mut SolveApp, tab: &str, id: &str) -> Result<(), String> {
if solvecraft_engine::find_command(id).is_none() {
if solidcraft_engine::find_command(id).is_none() {
return Err(format!("no command `{id}`"));
}
if pinned_on(app, tab, id).is_some() {
@@ -197,11 +197,12 @@ pub fn run(app: &mut SolveApp, action: &str, p: &Value) {
/// Shortcut keys, S box favourites and the toolbar as one file (Keyboard Shortcuts › Export).
pub fn export(app: &SolveApp) -> Value {
json!({ "format": "solvecraft-shortcuts/1", "shortcuts": app.keymap.prefs(), "favourites": app.sbox.pinned, "toolbar": app.toolbar_custom.prefs() })
json!({ "format": "solidcraft-shortcuts/1", "shortcuts": app.keymap.prefs(), "favourites": app.sbox.pinned, "toolbar": app.toolbar_custom.prefs() })
}
pub fn import(app: &mut SolveApp, v: &Value) -> Result<(), String> {
if v.get("format").and_then(Value::as_str) != Some("solvecraft-shortcuts/1") {
// "solvecraft-shortcuts/1": files exported before the rename (compatibility shim).
if !matches!(v.get("format").and_then(Value::as_str), Some("solidcraft-shortcuts/1" | "solvecraft-shortcuts/1")) {
return Err("not a SolidCraft shortcuts file".into());
}
if let Some(s) = v.get("shortcuts") {
+15 -15
View File
@@ -3,7 +3,7 @@
use egui::{Pos2, Stroke};
use serde_json::{Value, json};
use solvecraft_engine::geom::Vec2;
use solidcraft_engine::geom::Vec2;
use crate::SolveApp;
use crate::viewport::{Hit, Proj, pick, sketch_point_at};
@@ -336,8 +336,8 @@ fn run_shape(app: &mut SolveApp, t: &Tool) -> Result<Vec<String>, String> {
}
/// Only consume a complete, unambiguous selection; command validation owns geometry.
pub(crate) fn constraint_preselection(command: &str, selection: &[solvecraft_engine::Sel]) -> Option<Value> {
use solvecraft_engine::Sel::{SketchCurve, SketchPoint};
pub(crate) fn constraint_preselection(command: &str, selection: &[solidcraft_engine::Sel]) -> Option<Value> {
use solidcraft_engine::Sel::{SketchCurve, SketchPoint};
let selected_ids = |curves_only: bool, minimum: usize| -> Option<Vec<&str>> {
if !(minimum..=200).contains(&selection.len()) {
return None;
@@ -360,7 +360,7 @@ pub(crate) fn constraint_preselection(command: &str, selection: &[solvecraft_eng
("sketch.constraint.horizontal_vertical", [SketchCurve { id }]) => Some(json!({"line":id})),
("sketch.constraint.horizontal_vertical", [SketchPoint { id: a }, SketchPoint { id: b }]) if a != b => Some(json!({"points":[a,b]})),
("sketch.constraint.coincident", [a, b]) => {
let entity = |s: &solvecraft_engine::Sel| match s {
let entity = |s: &solidcraft_engine::Sel| match s {
SketchPoint { id } | SketchCurve { id } => Some(id.clone()),
_ => None,
};
@@ -390,8 +390,8 @@ pub(crate) fn constraint_preselection(command: &str, selection: &[solvecraft_eng
}
}
pub(crate) fn projection_preselection(selection: &[solvecraft_engine::Sel]) -> Option<Vec<Value>> {
use solvecraft_engine::Sel;
pub(crate) fn projection_preselection(selection: &[solidcraft_engine::Sel]) -> Option<Vec<Value>> {
use solidcraft_engine::Sel;
// The projection command processes at most 1000 references; never consume a subset.
if selection.is_empty() || selection.len() > 1000 {
return None;
@@ -451,23 +451,23 @@ pub fn finish(app: &mut SolveApp) {
/// Rubber-band preview of the shape being drawn.
pub(crate) fn paint_preview_curve(
sketch: &solvecraft_engine::sketch::Sketch,
plane: &solvecraft_engine::geom::Plane,
sketch: &solidcraft_engine::sketch::Sketch,
plane: &solidcraft_engine::geom::Plane,
painter: &egui::Painter,
proj: &Proj,
stroke: Stroke,
) {
paint_curve(sketch, 0, plane, painter, proj, stroke, &mut solvecraft_engine::render::SamplingBudget::new(4096, 65_536));
paint_curve(sketch, 0, plane, painter, proj, stroke, &mut solidcraft_engine::render::SamplingBudget::new(4096, 65_536));
}
pub(crate) fn paint_curve(
sketch: &solvecraft_engine::sketch::Sketch,
sketch: &solidcraft_engine::sketch::Sketch,
index: usize,
plane: &solvecraft_engine::geom::Plane,
plane: &solidcraft_engine::geom::Plane,
painter: &egui::Painter,
proj: &Proj,
stroke: Stroke,
budget: &mut solvecraft_engine::render::SamplingBudget,
budget: &mut solidcraft_engine::render::SamplingBudget,
) {
for part in crate::sketch_display::curve_with_budget(sketch, index, plane, proj, painter.ctx().pixels_per_point(), budget).parts {
let screen: Vec<_> = part.into_iter().filter_map(|point| proj.to_screen(point)).collect();
@@ -479,9 +479,9 @@ pub(crate) fn paint_curve(
/// Temporary analytic geometry uses the same exact span extraction as committed curves.
/// It never solves constraints or modifies the document.
pub(crate) fn preview_curve(t: &Tool, hover: Vec2, sweep: Option<f64>) -> Option<solvecraft_engine::sketch::Sketch> {
use solvecraft_engine::sketch::{CurveKind, Sketch};
if !hover.is_finite() || t.pts.len() >= solvecraft_engine::sketch::MAX_SPLINE_POINTS || t.pts.iter().any(|p| !p.0.is_finite()) {
pub(crate) fn preview_curve(t: &Tool, hover: Vec2, sweep: Option<f64>) -> Option<solidcraft_engine::sketch::Sketch> {
use solidcraft_engine::sketch::{CurveKind, Sketch};
if !hover.is_finite() || t.pts.len() >= solidcraft_engine::sketch::MAX_SPLINE_POINTS || t.pts.iter().any(|p| !p.0.is_finite()) {
return None;
}
let mut sk = Sketch::new();
+67 -67
View File
@@ -3,11 +3,11 @@
use egui::{Align2, Color32, FontId, Pos2, Rect, Sense, Shape, Stroke, pos2, vec2};
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::CurveKind;
use solvecraft_engine::view::{colors, grid_step, sketch_consumed, sketch_lines_with_sampler};
use solidcraft_engine::Sel;
use solidcraft_engine::geom::{Vec2, Vec3};
use solidcraft_engine::render::{Camera, Mat4, Rgb};
use solidcraft_engine::sketch::CurveKind;
use solidcraft_engine::view::{colors, grid_step, sketch_consumed, sketch_lines_with_sampler};
use std::hash::{Hash, Hasher};
use crate::gpu::{GpuScene, GpuTarget, SceneSlot, ViewportCallback};
@@ -29,7 +29,7 @@ pub enum NavMode {
#[derive(Default)]
pub struct ViewportState {
/// Per-body bounding boxes for fast picking, keyed by the model they were taken from.
pick_bounds: std::sync::Mutex<Option<(usize, std::sync::Arc<Vec<(String, solvecraft_engine::geom::Aabb3)>>)>>,
pick_bounds: std::sync::Mutex<Option<(usize, std::sync::Arc<Vec<(String, solidcraft_engine::geom::Aabb3)>>)>>,
/// The last pick (its key and hits): reused while nothing it depends on changed.
last_pick: Option<(u64, Vec<Hit>)>,
pub rect: Option<Rect>,
@@ -325,7 +325,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
let hidden_col = c4(tk.text.gamma_multiply(0.35));
// Appearances: the body's opacity, and faces with looks of their own.
let opacity = s.doc.model_body_opacity(b);
let face_looks: std::collections::HashMap<usize, solvecraft_engine::doc::appearance::Look> =
let face_looks: std::collections::HashMap<usize, solidcraft_engine::doc::appearance::Look> =
if shaded { s.doc.face_colors(b).into_iter().collect() } else { Default::default() };
for (ti, t) in m.triangles.iter().enumerate().filter(|_| shaded) {
let (mut col, a) = match m.tri_face.get(ti).and_then(|f| face_looks.get(&(*f as usize))) {
@@ -371,7 +371,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
if let Some((o, n)) = section_plane(app) {
let col = c4(tk.text.gamma_multiply(0.9));
for b in st.bodies.iter().filter(|b| app.session.body_visible(&b.name)) {
for (a, c) in solvecraft_engine::view::section_segments(&b.mesh(), o, n) {
for (a, c) in solidcraft_engine::view::section_segments(&b.mesh(), o, n) {
sc.line(a.to_f32(), c.to_f32(), col, 1.8, false);
}
}
@@ -444,9 +444,9 @@ fn build_sketch_scene(app: &SolveApp, proj: &Proj) -> GpuScene {
sc
}
fn ground_shadow(sc: &mut GpuScene, app: &SolveApp, st: &solvecraft_engine::doc::ModelState) {
fn ground_shadow(sc: &mut GpuScene, app: &SolveApp, st: &solidcraft_engine::doc::ModelState) {
// The bodies' box in the camera's Z-up frame (the ground is its XY plane).
let mut b = solvecraft_engine::geom::Aabb3::EMPTY;
let mut b = solidcraft_engine::geom::Aabb3::EMPTY;
for x in st.bodies.iter().filter(|x| app.session.body_visible(&x.name)) {
let bb = x.mesh().bounds();
if bb.is_empty() {
@@ -561,8 +561,8 @@ pub fn origin_axes(app: &SolveApp) -> Vec<(&'static str, Vec3)> {
/// from, to).
pub fn construction_axis_segments(app: &SolveApp) -> Vec<(String, Vec3, Vec3)> {
let h = origin_size(app.cam.half_height()) * AXIS_LEN;
let bb = solvecraft_engine::view::bounds(&app.session);
solvecraft_engine::view::construction_axes(&app.session)
let bb = solidcraft_engine::view::bounds(&app.session);
solidcraft_engine::view::construction_axes(&app.session)
.into_iter()
.filter(|(_, n, _, _)| !app.session.visibility.hidden_origin.contains(n))
.map(|(_, name, o, d)| {
@@ -586,7 +586,7 @@ pub fn construction_axis_segments(app: &SolveApp) -> Vec<(String, Vec3, Vec3)> {
/// Construction points shown: (name, position).
pub fn construction_points(app: &SolveApp) -> Vec<(String, Vec3)> {
solvecraft_engine::view::construction_points(&app.session)
solidcraft_engine::view::construction_points(&app.session)
.into_iter()
.filter(|(_, n, _)| !app.session.visibility.hidden_origin.contains(n))
.map(|(_, n, p)| (n, p))
@@ -596,7 +596,7 @@ pub fn construction_points(app: &SolveApp) -> Vec<(String, Vec3)> {
/// Construction planes drawn as squares around their origin: (name, normal, quad).
pub fn construction_quads(app: &SolveApp) -> Vec<(String, Vec3, [Vec3; 4])> {
let h = origin_size(app.cam.half_height()) * 1.2;
solvecraft_engine::view::construction_planes(&app.session)
solidcraft_engine::view::construction_planes(&app.session)
.into_iter()
.filter(|(_, n, _)| !app.session.visibility.hidden_origin.contains(n))
.map(|(_, name, pl)| {
@@ -611,8 +611,8 @@ pub fn construction_quads(app: &SolveApp) -> Vec<(String, Vec3, [Vec3; 4])> {
/// Bounding boxes of the shown model's bodies (cached until the model changes).
fn body_bounds(
app: &SolveApp,
st: &std::sync::Arc<solvecraft_engine::doc::ModelState>,
) -> std::sync::Arc<Vec<(String, solvecraft_engine::geom::Aabb3)>> {
st: &std::sync::Arc<solidcraft_engine::doc::ModelState>,
) -> std::sync::Arc<Vec<(String, solidcraft_engine::geom::Aabb3)>> {
let key = std::sync::Arc::as_ptr(st) as usize;
if let Ok(mut g) = app.viewport.pick_bounds.lock() {
if let Some((k, b)) = g.as_ref()
@@ -628,9 +628,9 @@ fn body_bounds(
}
/// The model's extent: bodies (from the cached boxes) and sketches.
pub fn model_bounds(app: &SolveApp) -> solvecraft_engine::geom::Aabb3 {
pub fn model_bounds(app: &SolveApp) -> solidcraft_engine::geom::Aabb3 {
let st = app.session.world_state();
let mut b = body_bounds(app, &st).iter().fold(solvecraft_engine::geom::Aabb3::EMPTY, |a, (_, x)| a.union(x));
let mut b = body_bounds(app, &st).iter().fold(solidcraft_engine::geom::Aabb3::EMPTY, |a, (_, x)| a.union(x));
for ss in &st.sketches {
if let Some((lo, hi)) = ss.sketch.bounds() {
for p in [lo, hi] {
@@ -645,7 +645,7 @@ pub fn model_bounds(app: &SolveApp) -> solvecraft_engine::geom::Aabb3 {
}
/// Where a ray enters a box (None: it misses).
fn ray_box(o: Vec3, d: Vec3, b: &solvecraft_engine::geom::Aabb3, pad: f64) -> Option<f64> {
fn ray_box(o: Vec3, d: Vec3, b: &solidcraft_engine::geom::Aabb3, pad: f64) -> Option<f64> {
let (mut t0, mut t1) = (f64::NEG_INFINITY, f64::INFINITY);
for (oo, dd, lo, hi) in [(o.x, d.x, b.min.x, b.max.x), (o.y, d.y, b.min.y, b.max.y), (o.z, d.z, b.min.z, b.max.z)] {
let (lo, hi) = (lo - pad, hi + pad);
@@ -663,7 +663,7 @@ fn ray_box(o: Vec3, d: Vec3, b: &solvecraft_engine::geom::Aabb3, pad: f64) -> Op
}
/// A box's outline on screen, grown by `px` (None: behind the eye).
fn screen_box(proj: &Proj, b: &solvecraft_engine::geom::Aabb3, px: f32) -> Option<Rect> {
fn screen_box(proj: &Proj, b: &solidcraft_engine::geom::Aabb3, px: f32) -> Option<Rect> {
let mut r = Rect::NOTHING;
for i in 0..8 {
let p = Vec3::new(
@@ -917,7 +917,7 @@ fn pick_with_sketch_points(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_
/// The section cut shown now: the open Section Analysis dialog's plane, else the session's.
/// The active sketch's display options (defaults when not sketching).
pub fn active_view(app: &SolveApp) -> solvecraft_engine::sketch::SketchView {
pub fn active_view(app: &SolveApp) -> solidcraft_engine::sketch::SketchView {
let st = app.session.world_state();
app.session.active_sketch.and_then(|s| st.sketch(s)).map(|ss| ss.sketch.view.clone()).unwrap_or_default()
}
@@ -959,7 +959,7 @@ fn feature_geometry(app: &SolveApp, id: u64) -> Vec<Sel> {
out.push(Sel::Body { name: b.name.clone() });
continue;
}
let names = solvecraft_engine::doc::naming::face_names(b);
let names = solidcraft_engine::doc::naming::face_names(b);
let m = b.mesh();
for (index, _) in names.iter().enumerate().filter(|(_, n)| n.starts_with(&prefix)) {
if let Some(point) = m.triangles.iter().zip(&m.tri_face).find_map(|(t, f)| {
@@ -992,9 +992,9 @@ fn displayed_selection(app: &SolveApp, item: &Sel) -> Vec<Sel> {
/// The box around what is selected (bodies, faces, edges, vertices, sketch curves and the
/// bodies of selected features), where it is shown.
pub fn selection_bounds(app: &SolveApp) -> Option<solvecraft_engine::geom::Aabb3> {
pub fn selection_bounds(app: &SolveApp) -> Option<solidcraft_engine::geom::Aabb3> {
let st = app.session.world_state();
let mut b = solvecraft_engine::geom::Aabb3::EMPTY;
let mut b = solidcraft_engine::geom::Aabb3::EMPTY;
for s in app.session.selection.iter().flat_map(|item| displayed_selection(app, item)) {
match &s {
Sel::Body { name } => {
@@ -1056,7 +1056,7 @@ pub fn selection_bounds(app: &SolveApp) -> Option<solvecraft_engine::geom::Aabb3
/// The section as the shader's clip plane (normal, d), zero when there is none.
/// The surface analysis for the shader (see `gpu::ViewportCallback::analysis`).
fn analysis_uniform(app: &SolveApp) -> [f32; 8] {
use solvecraft_engine::SurfaceAnalysis as A;
use solidcraft_engine::SurfaceAnalysis as A;
match app.session.analysis {
Some(A::Zebra { stripes }) => [1.0, stripes as f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
Some(A::Draft { pull, angle }) => [2.0, angle as f32, 0.0, 0.0, pull.x as f32, pull.y as f32, pull.z as f32, 0.0],
@@ -1128,7 +1128,7 @@ pub fn candidate(app: &SolveApp, hits: &[Hit]) -> Option<(Hit, Sel)> {
/// The edge a selection refers to now: by index if it still passes through the stored point,
/// otherwise the edge nearest the point.
fn edge_of(m: &solvecraft_engine::geom::Mesh, index: usize, point: Vec3) -> Option<&Vec<Vec3>> {
fn edge_of(m: &solidcraft_engine::geom::Mesh, index: usize, point: Vec3) -> Option<&Vec<Vec3>> {
let tol = m.bounds().diagonal().max(1.0) * 1e-6;
let near = |e: &Vec<Vec3>| e.windows(2).map(|w| point.dist_to_segment(w[0], w[1])).fold(f64::INFINITY, f64::min);
if let Some(e) = m.edges.get(index)
@@ -1139,7 +1139,7 @@ fn edge_of(m: &solvecraft_engine::geom::Mesh, index: usize, point: Vec3) -> Opti
m.edges.iter().min_by(|a, b| near(a).total_cmp(&near(b))).filter(|e| near(e) < tol * 1e3)
}
fn face_tris(sc: &mut GpuScene, m: &solvecraft_engine::geom::Mesh, face: usize, col: [u8; 4], lit: bool) {
fn face_tris(sc: &mut GpuScene, m: &solidcraft_engine::geom::Mesh, face: usize, col: [u8; 4], lit: bool) {
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
if *f as usize != face {
continue;
@@ -1167,7 +1167,7 @@ fn edge_lines(sc: &mut GpuScene, e: &[Vec3], core: Color32, halo: Color32, w: f3
}
/// The outline of a highlighted face where the model hides it (dimmed).
fn hidden_face_outline(sc: &mut GpuScene, m: &solvecraft_engine::geom::Mesh, face: usize, col: Color32) {
fn hidden_face_outline(sc: &mut GpuScene, m: &solidcraft_engine::geom::Mesh, face: usize, col: Color32) {
for (ei, e) in m.edges.iter().enumerate() {
let on = m.edge_faces.get(ei).is_some_and(|f| f.iter().any(|x| *x as usize == face));
if on && m.edge_pickable(ei) {
@@ -1189,7 +1189,7 @@ pub(crate) fn profile_fill(sc: &mut GpuScene, app: &SolveApp, sketch: u64, index
};
// Lift toward the eye so a profile on a face shows over it.
let lift = app.cam.back() * (app.cam.half_height() * 0.002);
let w = |q: solvecraft_engine::geom::Vec2| (plane.to_world(q) + lift).to_f32();
let w = |q: solidcraft_engine::geom::Vec2| (plane.to_world(q) + lift).to_f32();
let col = c4(fill);
// In the translucent pass (after the grid and edges) so it covers them.
let proj = display_proj(app);
@@ -1467,7 +1467,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
}
/// A body's display colour (appearance, material or imported colour), if it has one.
fn body_color(doc: &solvecraft_engine::doc::Document, b: &solvecraft_engine::doc::ModelBody) -> Option<Color32> {
fn body_color(doc: &solidcraft_engine::doc::Document, b: &solidcraft_engine::doc::ModelBody) -> Option<Color32> {
let to8 = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8;
doc.body_color(b).map(|[r, g, b]| Color32::from_rgb(to8(r), to8(g), to8(b)))
}
@@ -2080,7 +2080,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
let at = match app.session.execute("sketch.drag", &json!({"entity": id, "from": [from.x, from.y], "to": [q.x, q.y], "live": true})) {
Ok(r) => {
app.session.undo.truncate(depth + 1);
r.get("at").and_then(solvecraft_engine::params::vec2).unwrap_or(q)
r.get("at").and_then(solidcraft_engine::params::vec2).unwrap_or(q)
}
Err(_) => from,
};
@@ -2147,7 +2147,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
points_2d(app, &painter, &proj);
if let Some((plane, sketch)) = crate::sketch_attachment_ui::lost_guide(app)
&& let Some((low, high)) = sketch.bounds()
&& let Some(at) = proj.to_screen(plane.to_world(solvecraft_engine::geom::Vec2::new((low.x + high.x) * 0.5, high.y)))
&& let Some(at) = proj.to_screen(plane.to_world(solidcraft_engine::geom::Vec2::new((low.x + high.x) * 0.5, high.y)))
{
painter.text(at - vec2(0.0, 12.0), Align2::CENTER_BOTTOM, "Saved sketch plane — attachment lost", FontId::proportional(12.0), t.warning);
}
@@ -2209,7 +2209,7 @@ fn select(app: &mut SolveApp, sel: Option<Sel>, add: bool) {
/// Expand a visible rim or surface into the kernel pieces a command needs. The original
/// face pick keeps its point (used by tools such as Hole and construction geometry).
pub(crate) fn grouped_selection(s: &solvecraft_engine::Session, sel: &Sel) -> Vec<Sel> {
pub(crate) fn grouped_selection(s: &solidcraft_engine::Session, sel: &Sel) -> Vec<Sel> {
let st = s.world_state();
match sel {
Sel::Edge { body, index, .. } => {
@@ -2250,7 +2250,7 @@ pub(crate) fn grouped_selection(s: &solvecraft_engine::Session, sel: &Sel) -> Ve
}
/// Measure keeps one entry per visible item, rather than using its two slots for kernel halves.
pub(crate) fn measurement_selection(s: &solvecraft_engine::Session, sel: &Sel) -> Sel {
pub(crate) fn measurement_selection(s: &solidcraft_engine::Session, sel: &Sel) -> Sel {
let pieces = grouped_selection(s, sel);
let mut representative = sel.clone();
match &mut representative {
@@ -2301,7 +2301,7 @@ fn box_select(app: &mut SolveApp, proj: &Proj, bx: BoxSel, add: bool) {
let takes = if bx.crossing {
parts.iter().any(|part| bx.takes(part))
} else {
sampled.quality == solvecraft_engine::render::SamplingQuality::Complete
sampled.quality == solidcraft_engine::render::SamplingQuality::Complete
&& !parts.is_empty()
&& parts.iter().all(|part| bx.takes(part))
};
@@ -2310,7 +2310,7 @@ fn box_select(app: &mut SolveApp, proj: &Proj, bx: BoxSel, add: bool) {
}
}
for (i, p) in ss.sketch.points.iter().enumerate().filter(|_| !off("sketch") && !off("sketch_points")) {
if i != 0 && solvecraft_engine::view::sketch_point_visible(&ss.sketch, i) && bx.takes(&to2(&[ss.plane.to_world(p.pos)])) {
if i != 0 && solidcraft_engine::view::sketch_point_visible(&ss.sketch, i) && bx.takes(&to2(&[ss.plane.to_world(p.pos)])) {
out.push(Sel::SketchPoint { id: p.id.clone() });
}
}
@@ -2483,7 +2483,7 @@ fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
let locked = sk.locked_points();
let virtual_corners: Vec<usize> = sk.virtual_corners().into_iter().map(|(i, _)| i).collect();
for (i, p) in sk.points.iter().enumerate() {
if !solvecraft_engine::view::sketch_point_visible(sk, i) {
if !solidcraft_engine::view::sketch_point_visible(sk, i) {
continue;
}
let hidden = (sk.view.hide_points && i != 0) || (sk.view.hide_projected && p.link.is_some());
@@ -2531,14 +2531,14 @@ fn cpu_render(app: &mut SolveApp, ctx: &egui::Context, painter: &egui::Painter,
(rect.width() as u32, rect.height() as u32).hash(&mut h);
let key = h.finish();
if app.viewport.cpu.as_ref().is_none_or(|(k, _)| *k != key) {
let mut budget = solvecraft_engine::render::SamplingBudget::new(131_072, 1_048_576);
let mut budget = solidcraft_engine::render::SamplingBudget::new(131_072, 1_048_576);
let scene =
solvecraft_engine::view::scene_with_section_and_sketch_sampler(&app.session, &proj.cam, section_plane(app), |sk, index, plane| {
solidcraft_engine::view::scene_with_section_and_sketch_sampler(&app.session, &proj.cam, section_plane(app), |sk, index, plane| {
crate::sketch_display::curve_with_budget(sk, index, plane, proj, app.viewport.pixels_per_point(), &mut budget).parts
});
let w = (rect.width() as usize).clamp(16, 4096);
let hh = (rect.height() as usize).clamp(16, 4096);
let c = solvecraft_engine::render::render(&scene, &proj.cam, w, hh);
let c = solidcraft_engine::render::render(&scene, &proj.cam, w, hh);
let img = egui::ColorImage::from_rgba_unmultiplied([c.w, c.h], &c.rgba);
let tex = ctx.load_texture("sc_cpu_viewport", img, egui::TextureOptions::LINEAR);
app.viewport.cpu = Some((key, tex));
@@ -2810,7 +2810,7 @@ fn nearest_quarter_turn(yaw: f64) -> f64 {
pub fn look_at_selection(app: &mut SolveApp) {
let target = app.session.selection.iter().find_map(|s| match s {
Sel::Face { body, index, point } => crate::dialogs::planar_face(&app.session, body, *index).map(|(_, n)| (*point, n)),
Sel::Plane { name } => solvecraft_engine::geom::Plane::named(name).map(|p| (p.origin, p.normal())),
Sel::Plane { name } => solidcraft_engine::geom::Plane::named(name).map(|p| (p.origin, p.normal())),
_ => None,
});
match target {
@@ -2963,7 +2963,7 @@ mod tests {
}
let mut h = crate::scenario::Harness::new();
h.app.run("solid.box", json!({"length":40,"width":30,"height":20})).unwrap();
let cam = solvecraft_engine::view::home_camera(&h.app.session);
let cam = solidcraft_engine::view::home_camera(&h.app.session);
let whole = pixels(&mut h, cam);
let before = h.app.session.doc.clone();
let undo = h.app.session.undo.len();
@@ -3031,7 +3031,7 @@ mod tests {
#[test]
fn timeline_modifier_highlight_follows_edits_and_rollback() {
let mut app = SolveApp::new(solvecraft_engine::Session::default(), crate::Services::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
app.run("solid.box", json!({"length":20,"width":20,"height":20})).unwrap();
let id = app.run("solid.fillet", json!({"edges":[[0,0,10]],"radius":2})).unwrap()["feature"].as_u64().unwrap();
// A later chamfer changes the same body without consuming the fillet.
@@ -3052,7 +3052,7 @@ mod tests {
#[test]
fn timeline_modifier_highlight_follows_component_instances() {
let mut app = SolveApp::new(solvecraft_engine::Session::default(), crate::Services::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
app.run("component.create", json!({"name":"Part"})).unwrap();
app.run("solid.box", json!({"length":20,"width":20,"height":20})).unwrap();
let id = app.run("solid.fillet", json!({"edges":[[0,0,10]],"radius":2})).unwrap()["feature"].as_u64().unwrap();
@@ -3082,7 +3082,7 @@ mod tests {
}
fn hover_app(gpu: bool) -> SolveApp {
let mut s = solvecraft_engine::Session::default();
let mut s = solidcraft_engine::Session::default();
s.execute("sketch.create", &json!({"plane": "XY"})).unwrap();
s.execute("sketch.point", &json!({"point": [0, 0]})).unwrap();
let mut app = SolveApp::new(s, crate::Services::default());
@@ -3242,7 +3242,7 @@ mod tests {
app.viewport.spin = false;
hover_frame(&mut app, &ctx, vec![]);
assert!(app.viewport.hover.is_some());
app.cam_anim = Some(solvecraft_engine::render::CameraAnim::new(app.cam, app.cam, 0.0));
app.cam_anim = Some(solidcraft_engine::render::CameraAnim::new(app.cam, app.cam, 0.0));
for _ in 0..3 {
hover_frame(&mut app, &ctx, vec![]);
assert!(app.viewport.hover.is_none());
@@ -3253,7 +3253,7 @@ mod tests {
}
fn cylinder_app() -> SolveApp {
let mut s = solvecraft_engine::Session::default();
let mut s = solidcraft_engine::Session::default();
s.execute("solid.cylinder", &json!({"radius": 10, "height": 20})).unwrap();
let mut app = SolveApp::new(s, crate::Services::default());
app.ui.show_origin = false;
@@ -3262,7 +3262,7 @@ mod tests {
#[test]
fn smooth_wall_display_preserves_wireframe_and_edge_selection() {
let mut s = solvecraft_engine::Session::default();
let mut s = solidcraft_engine::Session::default();
let sketch = s.execute("sketch.create", &json!({"plane":"XY"})).unwrap()["sketch"].as_u64().unwrap();
s.execute("sketch.ellipse", &json!({"center":[0,0],"major":[25,0],"minor_radius":5})).unwrap();
let inner =
@@ -3299,22 +3299,22 @@ mod tests {
let mut without_display_mask = (*mesh).clone();
without_display_mask.smooth_edges.clear();
assert_eq!(mesh.surface_patch(mesh.edge_faces[smooth][0] as usize), without_display_mask.surface_patch(mesh.edge_faces[smooth][0] as usize));
let cam = solvecraft_engine::view::home_camera(&app.session);
let cpu = solvecraft_engine::view::scene(&app.session, &cam);
let actual_edges = cpu.lines.iter().filter(|line| line.color == solvecraft_engine::view::colors::EDGE).count();
let cam = solidcraft_engine::view::home_camera(&app.session);
let cpu = solidcraft_engine::view::scene(&app.session, &cam);
let actual_edges = cpu.lines.iter().filter(|line| line.color == solidcraft_engine::view::colors::EDGE).count();
assert_eq!(actual_edges, mesh.edges.iter().enumerate().filter(|(i, _)| mesh.shaded_edge_visible(*i)).count());
if let Ok(directory) = std::env::var("SOLVECRAFT_SMOOTH_SCREENSHOTS") {
if let Ok(directory) = solidcraft_engine::geom::env::var("SMOOTH_SCREENSHOTS").ok_or(()) {
std::fs::create_dir_all(&directory).unwrap();
for (name, old_edges, wireframe) in [("after", false, false), ("before", true, false), ("wireframe", true, true)] {
let mut scene = solvecraft_engine::view::scene(&app.session, &cam);
scene.lines.retain(|line| line.color == solvecraft_engine::view::colors::EDGE);
let mut scene = solidcraft_engine::view::scene(&app.session, &cam);
scene.lines.retain(|line| line.color == solidcraft_engine::view::colors::EDGE);
if old_edges {
scene.lines.clear();
for (i, edge) in mesh.edges.iter().enumerate() {
if mesh.edge_pickable(i) {
scene.lines.push(solvecraft_engine::render::SceneLine {
scene.lines.push(solidcraft_engine::render::SceneLine {
points: edge.clone(),
color: solvecraft_engine::view::colors::EDGE,
color: solidcraft_engine::view::colors::EDGE,
width: 1.2,
on_top: false,
});
@@ -3324,7 +3324,7 @@ mod tests {
if wireframe {
scene.meshes.clear();
}
let png = solvecraft_engine::render::render_png(&scene, &cam, 1200, 800).unwrap();
let png = solidcraft_engine::render::render_png(&scene, &cam, 1200, 800).unwrap();
std::fs::write(std::path::Path::new(&directory).join(format!("smooth-ring-{name}.png")), png).unwrap();
}
}
@@ -3332,7 +3332,7 @@ mod tests {
#[test]
fn certified_small_crease_overrides_old_seam_fit_for_picking_and_patches() {
let mut s = solvecraft_engine::Session::default();
let mut s = solidcraft_engine::Session::default();
let sketch = s.execute("sketch.create", &json!({"plane":"XY"})).unwrap()["sketch"].as_u64().unwrap();
let step = std::f64::consts::TAU / 8.;
let k = 4. / 3. * (step / 4.).tan() * 10.;
@@ -3683,7 +3683,7 @@ mod tests {
#[test]
fn trackpad_gestures_over_dialogs_do_not_move_the_view() {
let ctx = egui::Context::default();
let mut app = SolveApp::new(solvecraft_engine::Session::default(), crate::Services::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
assert!(app.preferences.trackpad_pan);
app.viewport.no_pixels = true;
crate::help::run(&mut app, &ctx, "shortcuts");
@@ -3741,7 +3741,7 @@ mod tests {
fn shortcuts_window_wheel_does_not_zoom_the_viewport() {
for smooth in [true, false] {
let ctx = egui::Context::default();
let mut app = SolveApp::new(solvecraft_engine::Session::default(), crate::Services::default());
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
app.preferences.smooth_wheel_zoom = smooth;
app.preferences.trackpad_pan = false;
app.viewport.no_pixels = true;
@@ -3883,12 +3883,12 @@ mod tests {
#[test]
fn section_outlines_follow_the_cut() {
let mut s = solvecraft_engine::Session::default();
let mut s = solidcraft_engine::Session::default();
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
let st = s.model.state();
let m = st.bodies[0].mesh();
let (o, n) = (Vec3::new(0.0, 15.0, 0.0), Vec3::new(0.0, -1.0, 0.0));
let segs = solvecraft_engine::view::section_segments(&m, o, n);
let segs = solidcraft_engine::view::section_segments(&m, o, n);
assert!(segs.len() >= 4, "{segs:?}");
// On the plane (a hair to the kept side) and around the 40 x 20 cut.
let mut len = 0.0;
@@ -3898,12 +3898,12 @@ mod tests {
}
assert!((len - 120.0).abs() < 0.5, "outline length {len}");
// A plane that misses the body cuts nothing.
assert!(solvecraft_engine::view::section_segments(&m, Vec3::new(0.0, 99.0, 0.0), n).is_empty());
assert!(solidcraft_engine::view::section_segments(&m, Vec3::new(0.0, 99.0, 0.0), n).is_empty());
}
#[test]
fn selection_shows_where_the_model_hides_it() {
let mut s = solvecraft_engine::Session::default();
let mut s = solidcraft_engine::Session::default();
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
let mut app = SolveApp::new(s, crate::Services::default());
assert!(build_highlight(&app).hidden.is_empty());
@@ -3963,7 +3963,7 @@ mod tests {
#[test]
fn axis_snap_selects_all_directions_in_both_up_frames() {
assert_eq!(snap_directions().count(), 26);
for up in [solvecraft_engine::render::UpAxis::Z, solvecraft_engine::render::UpAxis::Y] {
for up in [solidcraft_engine::render::UpAxis::Z, solidcraft_engine::render::UpAxis::Y] {
for d in snap_directions() {
let mut cam = Camera { up, ..Camera::default() };
cam = cam.looking_from(cam.from_frame(d + Vec3::new(0.05, 0.03, 0.02)));
@@ -3986,7 +3986,7 @@ mod tests {
assert!(h.app.viewport.orbit_snap.is_some());
match reset {
0 => h.app.fit_view(),
1 => h.app.set_up_axis(solvecraft_engine::render::UpAxis::Y),
1 => h.app.set_up_axis(solidcraft_engine::render::UpAxis::Y),
_ => crate::documents::new_design(&mut h.app),
}
let before = h.app.cam;
@@ -4092,7 +4092,7 @@ mod tests {
#[test]
fn construction_axes_span_the_model_and_points_show() {
let mut s = solvecraft_engine::Session::default();
let mut s = solidcraft_engine::Session::default();
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
s.execute("construct.axis.two_points", &json!({"a": {"vertex": [0, 0, 20]}, "b": {"vertex": [40, 30, 20]}})).unwrap();
s.execute("construct.point.vertex", &json!({"point": {"vertex": [40, 0, 20]}})).unwrap();
@@ -1,7 +1,7 @@
use super::build_scene;
use crate::{Services, SolveApp};
use serde_json::json;
use solvecraft_engine::{Session, doc, geom::Vec3, kernel};
use solidcraft_engine::{Session, doc, geom::Vec3, kernel};
#[test]
fn placed_step_body_uses_imported_opacity_until_a_design_look_overrides_it() {
@@ -24,7 +24,7 @@ fn placed_step_body_uses_imported_opacity_until_a_design_look_overrides_it() {
app.ui.ground_shadow = false;
app.ui.show_origin = false;
let placed = app.session.world_state().bodies.iter().find(|b| b.name != source).unwrap().name.clone();
app.session.selection = vec![solvecraft_engine::Sel::Body { name: placed }];
app.session.selection = vec![solidcraft_engine::Sel::Body { name: placed }];
app.start("appearance.assign");
let crate::dialogs::Kind::Appearance(appearance) = &app.dialog.as_ref().unwrap().kind else { panic!("appearance dialog") };
assert!((appearance.opacity - 0.4).abs() < 1e-6);
+2 -2
View File
@@ -1,7 +1,7 @@
//! Floating controls reserve drawing space without changing rendering or picking projection.
use egui::{Rect, pos2, vec2};
use solvecraft_engine::geom::{Aabb3, Vec3};
use solvecraft_engine::render::Camera;
use solidcraft_engine::geom::{Aabb3, Vec3};
use solidcraft_engine::render::Camera;
#[derive(Clone, Debug)]
pub(crate) struct Layout {
+1 -1
View File
@@ -1,7 +1,7 @@
use super::*;
use crate::scenario::Harness;
use serde_json::json;
use solvecraft_engine::render::StandardView;
use solidcraft_engine::render::StandardView;
#[test]
fn thin_rotated_bounds_fit_using_the_original_full_projection() {
+3 -3
View File
@@ -4,8 +4,8 @@
//! or Joint appear on several tabs. Commands registered on such a panel but not listed follow at
//! the end.
use solvecraft_engine::CommandSpec;
use solvecraft_engine::doc::FeatureKind;
use solidcraft_engine::CommandSpec;
use solidcraft_engine::doc::FeatureKind;
const ASSEMBLE: &[&str] = &[
"component.create",
@@ -143,7 +143,7 @@ mod tests {
use super::*;
fn ids(tab: &str, panel: &str) -> Vec<&'static str> {
let specs = solvecraft_engine::command_specs();
let specs = solidcraft_engine::command_specs();
layout(tab, panel, &specs).map(|(v, _)| v.iter().map(|c| c.id).collect()).unwrap_or_default()
}