forked from 1b-agent/opencadstudio-upstream
style: apply cargo fmt
This commit is contained in:
1 parent
07f4d1713f
commit
4bbac4c090
113 files changed
+3667
-1487
No files matched your search
+17
-14
@@ -28,7 +28,9 @@ pub const PROTOCOL_VERSION: u16 = 5;
|
|||||||
/// owns that app-side half of the pipeline.
|
/// owns that app-side half of the pipeline.
|
||||||
pub fn serialize_document(doc: &CadDocument, ext: &str) -> Result<Vec<u8>, String> {
|
pub fn serialize_document(doc: &CadDocument, ext: &str) -> Result<Vec<u8>, String> {
|
||||||
match ext.to_lowercase().as_str() {
|
match ext.to_lowercase().as_str() {
|
||||||
"dxf" => DxfWriter::new(doc).write_to_vec().map_err(|e| e.to_string()),
|
"dxf" => DxfWriter::new(doc)
|
||||||
|
.write_to_vec()
|
||||||
|
.map_err(|e| e.to_string()),
|
||||||
_ => {
|
_ => {
|
||||||
let mut buf = std::io::Cursor::new(Vec::new());
|
let mut buf = std::io::Cursor::new(Vec::new());
|
||||||
DwgWriter::write_to_writer(&mut buf, doc).map_err(|e| e.to_string())?;
|
DwgWriter::write_to_writer(&mut buf, doc).map_err(|e| e.to_string())?;
|
||||||
@@ -214,21 +216,18 @@ mod tests {
|
|||||||
let common = point.common();
|
let common = point.common();
|
||||||
assert_eq!(common.layer, "RUNTIME-FIELDS");
|
assert_eq!(common.layer, "RUNTIME-FIELDS");
|
||||||
assert_eq!(common.linetype_handle.map(|h| h.value()), Some(0xABCD));
|
assert_eq!(common.linetype_handle.map(|h| h.value()), Some(0xABCD));
|
||||||
assert_eq!(
|
assert_eq!(common.color_book_handle.map(|h| h.value()), Some(0x101));
|
||||||
common.color_book_handle.map(|h| h.value()),
|
|
||||||
Some(0x101)
|
|
||||||
);
|
|
||||||
assert_eq!(common.material_flags, 0b11);
|
assert_eq!(common.material_flags, 0b11);
|
||||||
assert_eq!(common.material_handle.map(|h| h.value()), Some(0x202));
|
assert_eq!(common.material_handle.map(|h| h.value()), Some(0x202));
|
||||||
assert_eq!(common.plotstyle_flags, 0b11);
|
assert_eq!(common.plotstyle_flags, 0b11);
|
||||||
assert_eq!(
|
assert_eq!(common.plotstyle_handle.map(|h| h.value()), Some(0x303));
|
||||||
common.plotstyle_handle.map(|h| h.value()),
|
|
||||||
Some(0x303)
|
|
||||||
);
|
|
||||||
assert_eq!(common.shadow_flags, 7);
|
assert_eq!(common.shadow_flags, 7);
|
||||||
assert_eq!(common.entity_mode, Some(2));
|
assert_eq!(common.entity_mode, Some(2));
|
||||||
assert!(common.has_ds_data);
|
assert!(common.has_ds_data);
|
||||||
assert_eq!(common.graphic_data.as_deref(), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
|
assert_eq!(
|
||||||
|
common.graphic_data.as_deref(),
|
||||||
|
Some(&[0xDE, 0xAD, 0xBE, 0xEF][..])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -237,9 +236,10 @@ mod tests {
|
|||||||
.expect("decode")
|
.expect("decode")
|
||||||
.expect("inner ok");
|
.expect("inner ok");
|
||||||
assert_eq!(ok, vec![1, 2, 3]);
|
assert_eq!(ok, vec![1, 2, 3]);
|
||||||
let err = decode_response(&encode_response(Err("writer failed".to_string())).expect("encode"))
|
let err =
|
||||||
.expect("decode")
|
decode_response(&encode_response(Err("writer failed".to_string())).expect("encode"))
|
||||||
.expect_err("inner err");
|
.expect("decode")
|
||||||
|
.expect_err("inner err");
|
||||||
assert_eq!(err, "writer failed");
|
assert_eq!(err, "writer failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +270,10 @@ mod tests {
|
|||||||
doc.version = codec::DxfVersion::AC1032;
|
doc.version = codec::DxfVersion::AC1032;
|
||||||
let dxf = serialize_document(&doc, "dxf").expect("dxf bytes");
|
let dxf = serialize_document(&doc, "dxf").expect("dxf bytes");
|
||||||
assert!(!dxf.is_empty());
|
assert!(!dxf.is_empty());
|
||||||
assert!(dxf.starts_with(b" 0") || dxf.starts_with(b"0\n"), "not a DXF header");
|
assert!(
|
||||||
|
dxf.starts_with(b" 0") || dxf.starts_with(b"0\n"),
|
||||||
|
"not a DXF header"
|
||||||
|
);
|
||||||
let dwg = serialize_document(&doc, "dwg").expect("dwg bytes");
|
let dwg = serialize_document(&doc, "dwg").expect("dwg bytes");
|
||||||
assert!(dwg.starts_with(b"AC10"), "not a DWG sentinel");
|
assert!(dwg.starts_with(b"AC10"), "not a DWG sentinel");
|
||||||
// Case-insensitive: the tab state and save dialog both pass through.
|
// Case-insensitive: the tab state and save dialog both pass through.
|
||||||
|
|||||||
+15
-11
@@ -319,7 +319,9 @@ fn workbook_metadata(path: &Path) -> Result<(Vec<String>, Vec<String>), String>
|
|||||||
return workbook_metadata_bytes(&bytes);
|
return workbook_metadata_bytes(&bytes);
|
||||||
}
|
}
|
||||||
if cfg!(target_arch = "wasm32") {
|
if cfg!(target_arch = "wasm32") {
|
||||||
return Err(crate::t!("Pick the file again to load its contents in the browser.").into_owned());
|
return Err(
|
||||||
|
crate::t!("Pick the file again to load its contents in the browser.").into_owned(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
use calamine::{open_workbook_auto, Reader};
|
use calamine::{open_workbook_auto, Reader};
|
||||||
let workbook = open_workbook_auto(path).map_err(|error| error.to_string())?;
|
let workbook = open_workbook_auto(path).map_err(|error| error.to_string())?;
|
||||||
@@ -336,7 +338,8 @@ fn workbook_metadata(path: &Path) -> Result<(Vec<String>, Vec<String>), String>
|
|||||||
/// the web, where no path exists to reopen).
|
/// the web, where no path exists to reopen).
|
||||||
fn workbook_metadata_bytes(bytes: &[u8]) -> Result<(Vec<String>, Vec<String>), String> {
|
fn workbook_metadata_bytes(bytes: &[u8]) -> Result<(Vec<String>, Vec<String>), String> {
|
||||||
use calamine::{open_workbook_auto_from_rs, Reader};
|
use calamine::{open_workbook_auto_from_rs, Reader};
|
||||||
let workbook = open_workbook_auto_from_rs(Cursor::new(bytes)).map_err(|error| error.to_string())?;
|
let workbook =
|
||||||
|
open_workbook_auto_from_rs(Cursor::new(bytes)).map_err(|error| error.to_string())?;
|
||||||
let sheets = workbook.sheet_names();
|
let sheets = workbook.sheet_names();
|
||||||
let names = workbook
|
let names = workbook
|
||||||
.defined_names()
|
.defined_names()
|
||||||
@@ -362,7 +365,9 @@ fn read_text_source(path: &Path) -> Result<String, String> {
|
|||||||
return Ok(String::from_utf8_lossy(&bytes).into_owned());
|
return Ok(String::from_utf8_lossy(&bytes).into_owned());
|
||||||
}
|
}
|
||||||
if cfg!(target_arch = "wasm32") {
|
if cfg!(target_arch = "wasm32") {
|
||||||
return Err(crate::t!("Pick the file again to load its contents in the browser.").into_owned());
|
return Err(
|
||||||
|
crate::t!("Pick the file again to load its contents in the browser.").into_owned(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
std::fs::read_to_string(path).map_err(|error| error.to_string())
|
std::fs::read_to_string(path).map_err(|error| error.to_string())
|
||||||
}
|
}
|
||||||
@@ -387,8 +392,9 @@ fn workbook_rows<RS: std::io::Read + std::io::Seek>(
|
|||||||
.find(|(candidate, _)| candidate.eq_ignore_ascii_case(name))
|
.find(|(candidate, _)| candidate.eq_ignore_ascii_case(name))
|
||||||
.map(|(_, formula)| formula.clone())
|
.map(|(_, formula)| formula.clone())
|
||||||
.ok_or_else(|| crate::t!("The named range was not found.").into_owned())?;
|
.ok_or_else(|| crate::t!("The named range was not found.").into_owned())?;
|
||||||
named_range_target(&formula)
|
named_range_target(&formula).ok_or_else(|| {
|
||||||
.ok_or_else(|| crate::t!("The named range does not refer to a worksheet cell range.").into_owned())?
|
crate::t!("The named range does not refer to a worksheet cell range.").into_owned()
|
||||||
|
})?
|
||||||
} else {
|
} else {
|
||||||
let selected_sheet = sheet
|
let selected_sheet = sheet
|
||||||
.filter(|value| !value.trim().is_empty())
|
.filter(|value| !value.trim().is_empty())
|
||||||
@@ -442,7 +448,9 @@ pub(crate) fn read_tabular_file(
|
|||||||
.map_err(|error| error.to_string())?;
|
.map_err(|error| error.to_string())?;
|
||||||
workbook_rows(&mut workbook, sheet, range_kind, range_value)?
|
workbook_rows(&mut workbook, sheet, range_kind, range_value)?
|
||||||
} else if cfg!(target_arch = "wasm32") {
|
} else if cfg!(target_arch = "wasm32") {
|
||||||
return Err(crate::t!("Pick the file again to load its contents in the browser.").into_owned());
|
return Err(
|
||||||
|
crate::t!("Pick the file again to load its contents in the browser.").into_owned(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
use calamine::open_workbook_auto;
|
use calamine::open_workbook_auto;
|
||||||
let mut workbook = open_workbook_auto(path).map_err(|error| error.to_string())?;
|
let mut workbook = open_workbook_auto(path).map_err(|error| error.to_string())?;
|
||||||
@@ -2139,7 +2147,6 @@ mod tests {
|
|||||||
assert!(register_picked_source("a.csv", Arc::new(b"old".to_vec())));
|
assert!(register_picked_source("a.csv", Arc::new(b"old".to_vec())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2222,10 +2229,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn registered_bytes_match_by_file_name() {
|
fn registered_bytes_match_by_file_name() {
|
||||||
let key = format!("ocs-match-{}-{}.xlsx", std::process::id(), line!());
|
let key = format!("ocs-match-{}-{}.xlsx", std::process::id(), line!());
|
||||||
register_picked_source(
|
register_picked_source(&format!(r"C:\drawings\{key}"), Arc::new(Vec::new()));
|
||||||
&format!(r"C:\drawings\{key}"),
|
|
||||||
Arc::new(Vec::new()),
|
|
||||||
);
|
|
||||||
// Exact key…
|
// Exact key…
|
||||||
assert!(picked_source_bytes(&format!(r"C:\drawings\{key}")).is_some());
|
assert!(picked_source_bytes(&format!(r"C:\drawings\{key}")).is_some());
|
||||||
// …same file under a differently-cased bare name (the web stores only
|
// …same file under a differently-cased bare name (the web stores only
|
||||||
|
|||||||
+23
-28
@@ -474,10 +474,7 @@ pub(super) fn requested_save_target(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_generated_dimension_block_name(name: &str) -> bool {
|
fn is_generated_dimension_block_name(name: &str) -> bool {
|
||||||
let Some(suffix) = name
|
let Some(suffix) = name.strip_prefix("*D").or_else(|| name.strip_prefix("*d")) else {
|
||||||
.strip_prefix("*D")
|
|
||||||
.or_else(|| name.strip_prefix("*d"))
|
|
||||||
else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
!suffix.is_empty() && suffix.bytes().all(|byte| byte.is_ascii_digit())
|
!suffix.is_empty() && suffix.bytes().all(|byte| byte.is_ascii_digit())
|
||||||
@@ -488,9 +485,10 @@ fn is_generated_dimension_block_name(name: &str) -> bool {
|
|||||||
/// logical drawing entities; named block members remain part of the manifest.
|
/// logical drawing entities; named block members remain part of the manifest.
|
||||||
fn is_dimension_display_entity(document: &codec::CadDocument, entity: &codec::EntityType) -> bool {
|
fn is_dimension_display_entity(document: &codec::CadDocument, entity: &codec::EntityType) -> bool {
|
||||||
let owner = entity.common().owner_handle;
|
let owner = entity.common().owner_handle;
|
||||||
document.block_records.iter().any(|block| {
|
document
|
||||||
block.handle == owner && is_generated_dimension_block_name(&block.name)
|
.block_records
|
||||||
})
|
.iter()
|
||||||
|
.any(|block| block.handle == owner && is_generated_dimension_block_name(&block.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn document_manifest(document: &codec::CadDocument) -> Value {
|
fn document_manifest(document: &codec::CadDocument) -> Value {
|
||||||
@@ -1795,17 +1793,13 @@ mod tests {
|
|||||||
detail.handle = document.allocate_handle();
|
detail.handle = document.allocate_handle();
|
||||||
let detail_handle = detail.handle;
|
let detail_handle = detail.handle;
|
||||||
document.block_records.add(detail).unwrap();
|
document.block_records.add(detail).unwrap();
|
||||||
let mut detail_line = Line::from_points(
|
let mut detail_line =
|
||||||
Vector3::new(20.0, 0.0, 0.0),
|
Line::from_points(Vector3::new(20.0, 0.0, 0.0), Vector3::new(30.0, 0.0, 0.0));
|
||||||
Vector3::new(30.0, 0.0, 0.0),
|
|
||||||
);
|
|
||||||
detail_line.common.owner_handle = detail_handle;
|
detail_line.common.owner_handle = detail_handle;
|
||||||
document.add_entity(EntityType::Line(detail_line)).unwrap();
|
document.add_entity(EntityType::Line(detail_line)).unwrap();
|
||||||
|
|
||||||
let mut dimension = DimensionLinear::new(
|
let mut dimension =
|
||||||
Vector3::new(0.0, 0.0, 0.0),
|
DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
|
||||||
Vector3::new(10.0, 0.0, 0.0),
|
|
||||||
);
|
|
||||||
dimension.definition_point = Vector3::new(0.0, 5.0, 0.0);
|
dimension.definition_point = Vector3::new(0.0, 5.0, 0.0);
|
||||||
dimension.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
|
dimension.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
|
||||||
document
|
document
|
||||||
@@ -1826,10 +1820,8 @@ mod tests {
|
|||||||
|
|
||||||
let mut app = super::OpenCADStudio::new_for_test();
|
let mut app = super::OpenCADStudio::new_for_test();
|
||||||
app.automation_op(r#"{"op":"new"}"#);
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
let mut dimension = DimensionLinear::new(
|
let mut dimension =
|
||||||
Vector3::new(0.0, 0.0, 0.0),
|
DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
|
||||||
Vector3::new(10.0, 0.0, 0.0),
|
|
||||||
);
|
|
||||||
dimension.definition_point = Vector3::new(0.0, 5.0, 0.0);
|
dimension.definition_point = Vector3::new(0.0, 5.0, 0.0);
|
||||||
dimension.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
|
dimension.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
|
||||||
app.tabs[app.active_tab]
|
app.tabs[app.active_tab]
|
||||||
@@ -1865,10 +1857,8 @@ mod tests {
|
|||||||
|
|
||||||
let mut app = super::OpenCADStudio::new_for_test();
|
let mut app = super::OpenCADStudio::new_for_test();
|
||||||
app.automation_op(r#"{"op":"new"}"#);
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
let mut dimension = DimensionLinear::new(
|
let mut dimension =
|
||||||
Vector3::new(0.0, 0.0, 0.0),
|
DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
|
||||||
Vector3::new(10.0, 0.0, 0.0),
|
|
||||||
);
|
|
||||||
dimension.definition_point = Vector3::new(0.0, 5.0, 0.0);
|
dimension.definition_point = Vector3::new(0.0, 5.0, 0.0);
|
||||||
dimension.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
|
dimension.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
@@ -1900,9 +1890,8 @@ mod tests {
|
|||||||
let captured = crate::io::edit_lock::FileFingerprint::capture(&path).unwrap();
|
let captured = crate::io::edit_lock::FileFingerprint::capture(&path).unwrap();
|
||||||
assert_eq!(app.tabs[i].disk_fingerprint.as_ref(), Some(&captured));
|
assert_eq!(app.tabs[i].disk_fingerprint.as_ref(), Some(&captured));
|
||||||
|
|
||||||
let ordinary = app.automation_op(
|
let ordinary =
|
||||||
&serde_json::json!({"op":"save", "path":path}).to_string(),
|
app.automation_op(&serde_json::json!({"op":"save", "path":path}).to_string());
|
||||||
);
|
|
||||||
assert_eq!(ordinary["ok"], true, "{ordinary}");
|
assert_eq!(ordinary["ok"], true, "{ordinary}");
|
||||||
assert!(!ordinary["error"]
|
assert!(!ordinary["error"]
|
||||||
.as_str()
|
.as_str()
|
||||||
@@ -3838,7 +3827,10 @@ mod tests {
|
|||||||
// Snapper is now forced on with only Midpoint enabled
|
// Snapper is now forced on with only Midpoint enabled
|
||||||
assert!(app.snapper.snap_enabled);
|
assert!(app.snapper.snap_enabled);
|
||||||
assert_eq!(app.snapper.enabled.len(), 1);
|
assert_eq!(app.snapper.enabled.len(), 1);
|
||||||
assert!(app.snapper.enabled.contains(&crate::snap::SnapType::Midpoint));
|
assert!(app
|
||||||
|
.snapper
|
||||||
|
.enabled
|
||||||
|
.contains(&crate::snap::SnapType::Midpoint));
|
||||||
|
|
||||||
// Submit first point (0,0)
|
// Submit first point (0,0)
|
||||||
let _ = app.update(Message::CommandInput("0,0".to_string()));
|
let _ = app.update(Message::CommandInput("0,0".to_string()));
|
||||||
@@ -3851,7 +3843,10 @@ mod tests {
|
|||||||
let _ = app.update(Message::CommandInput("END".to_string()));
|
let _ = app.update(Message::CommandInput("END".to_string()));
|
||||||
let _ = app.update(Message::CommandSubmit);
|
let _ = app.update(Message::CommandSubmit);
|
||||||
assert!(app.snapper.snap_enabled);
|
assert!(app.snapper.snap_enabled);
|
||||||
assert!(app.snapper.enabled.contains(&crate::snap::SnapType::Endpoint));
|
assert!(app
|
||||||
|
.snapper
|
||||||
|
.enabled
|
||||||
|
.contains(&crate::snap::SnapType::Endpoint));
|
||||||
|
|
||||||
// Escape cancels LINE and clears unconsumed snap override
|
// Escape cancels LINE and clears unconsumed snap override
|
||||||
let _ = app.update(Message::CommandEscape);
|
let _ = app.update(Message::CommandEscape);
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ fn main_depth(app: &OpenCADStudio) -> usize {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_undo_preserves_imported_native_networks_when_constraints_are_unchanged() {
|
fn local_undo_preserves_imported_native_networks_when_constraints_are_unchanged() {
|
||||||
use codec::objects::{AssocNetwork, AssociativeData, AssociativeObject, Dictionary, ObjectType};
|
use codec::objects::{
|
||||||
|
AssocNetwork, AssociativeData, AssociativeObject, Dictionary, ObjectType,
|
||||||
|
};
|
||||||
let (mut app, reference) = fixture();
|
let (mut app, reference) = fixture();
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
let doc = &mut app.tabs[i].scene.document;
|
let doc = &mut app.tabs[i].scene.document;
|
||||||
|
|||||||
@@ -889,7 +889,9 @@ impl OpenCADStudio {
|
|||||||
CmdResult::Show { p1, p2 } => {
|
CmdResult::Show { p1, p2 } => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.tabs[i].scene.remember_current_view();
|
self.tabs[i].scene.remember_current_view();
|
||||||
self.tabs[i].scene.zoom_to_window(p1.as_vec3(), p2.as_vec3());
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.zoom_to_window(p1.as_vec3(), p2.as_vec3());
|
||||||
}
|
}
|
||||||
CmdResult::CommitAndZoom { entity, p1, p2 } => {
|
CmdResult::CommitAndZoom { entity, p1, p2 } => {
|
||||||
self.handle_commit_entities_and_exit(vec![entity], false, false);
|
self.handle_commit_entities_and_exit(vec![entity], false, false);
|
||||||
|
|||||||
@@ -114,14 +114,14 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let created = match history {
|
let created = match history {
|
||||||
Some(history) => {
|
Some(history) => self.add_surface_model_with_history(surface, body, history),
|
||||||
self.add_surface_model_with_history(surface, body, history)
|
|
||||||
}
|
|
||||||
None => self.add_surface_model(surface, body),
|
None => self.add_surface_model(surface, body),
|
||||||
};
|
};
|
||||||
// An associative surface keeps reading its profile.
|
// An associative surface keeps reading its profile.
|
||||||
if sweep_model::surface_associativity() && path.is_none() && !created.is_null() {
|
if sweep_model::surface_associativity() && path.is_none() && !created.is_null() {
|
||||||
use crate::scene::model::surface_sources::{link_surface_sources, SurfaceSources};
|
use crate::scene::model::surface_sources::{
|
||||||
|
link_surface_sources, SurfaceSources,
|
||||||
|
};
|
||||||
let (height, given) = match extent {
|
let (height, given) = match extent {
|
||||||
ExtrudeExtent::Height(height) => (height, None),
|
ExtrudeExtent::Height(height) => (height, None),
|
||||||
_ => (direction.length(), Some(direction)),
|
_ => (direction.length(), Some(direction)),
|
||||||
@@ -129,7 +129,12 @@ impl OpenCADStudio {
|
|||||||
link_surface_sources(
|
link_surface_sources(
|
||||||
&mut self.tabs[i].scene.document,
|
&mut self.tabs[i].scene.document,
|
||||||
created,
|
created,
|
||||||
&SurfaceSources::Extrude { profile: *handle, height, direction: given, taper: taper_angle },
|
&SurfaceSources::Extrude {
|
||||||
|
profile: *handle,
|
||||||
|
height,
|
||||||
|
direction: given,
|
||||||
|
taper: taper_angle,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
associative_sources.push(*handle);
|
associative_sources.push(*handle);
|
||||||
}
|
}
|
||||||
@@ -385,18 +390,14 @@ impl OpenCADStudio {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let created = self.add_surface_model(
|
let created = self.add_surface_model(
|
||||||
empty_revolved_surface(
|
empty_revolved_surface(&entity, axis_start, axis_end, angle, start_angle),
|
||||||
&entity,
|
|
||||||
axis_start,
|
|
||||||
axis_end,
|
|
||||||
angle,
|
|
||||||
start_angle,
|
|
||||||
),
|
|
||||||
body,
|
body,
|
||||||
);
|
);
|
||||||
// An associative surface keeps reading its profile and axis.
|
// An associative surface keeps reading its profile and axis.
|
||||||
if sweep_model::surface_associativity() && !created.is_null() {
|
if sweep_model::surface_associativity() && !created.is_null() {
|
||||||
use crate::scene::model::surface_sources::{link_surface_sources, SurfaceSources};
|
use crate::scene::model::surface_sources::{
|
||||||
|
link_surface_sources, SurfaceSources,
|
||||||
|
};
|
||||||
link_surface_sources(
|
link_surface_sources(
|
||||||
&mut self.tabs[i].scene.document,
|
&mut self.tabs[i].scene.document,
|
||||||
created,
|
created,
|
||||||
@@ -758,7 +759,9 @@ impl OpenCADStudio {
|
|||||||
crate::scene::model::surface_sources::link_surface_sources(
|
crate::scene::model::surface_sources::link_surface_sources(
|
||||||
&mut self.tabs[i].scene.document,
|
&mut self.tabs[i].scene.document,
|
||||||
handle,
|
handle,
|
||||||
&crate::scene::model::surface_sources::SurfaceSources::Loft { sections: single },
|
&crate::scene::model::surface_sources::SurfaceSources::Loft {
|
||||||
|
sections: single,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
associative = true;
|
associative = true;
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-15
@@ -104,12 +104,14 @@ impl OpenCADStudio {
|
|||||||
) -> Box<dyn CadCommand> {
|
) -> Box<dyn CadCommand> {
|
||||||
let document = &self.tabs[i].scene.document;
|
let document = &self.tabs[i].scene.document;
|
||||||
let defaults = crate::scene::creation_style::current_text_defaults(document);
|
let defaults = crate::scene::creation_style::current_text_defaults(document);
|
||||||
Box::new(crate::modules::draw::clipboard::pastespec::PasteSpecCommand::new(
|
Box::new(
|
||||||
!self.clipboard.is_empty(),
|
crate::modules::draw::clipboard::pastespec::PasteSpecCommand::new(
|
||||||
text,
|
!self.clipboard.is_empty(),
|
||||||
defaults.style_name,
|
text,
|
||||||
defaults.height,
|
defaults.style_name,
|
||||||
))
|
defaults.height,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn dispatch_blocks(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
pub(super) fn dispatch_blocks(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||||
@@ -319,20 +321,22 @@ impl OpenCADStudio {
|
|||||||
// ARRAYEDIT Source: edit the objects the array repeats in the
|
// ARRAYEDIT Source: edit the objects the array repeats in the
|
||||||
// Block Editor; every item follows when it is saved.
|
// Block Editor; every item follows when it is saved.
|
||||||
cmd if cmd.starts_with("ARRAYEDIT_SOURCE:") => {
|
cmd if cmd.starts_with("ARRAYEDIT_SOURCE:") => {
|
||||||
let handle = codec::Handle::new(
|
let handle =
|
||||||
cmd["ARRAYEDIT_SOURCE:".len()..].parse().unwrap_or(0),
|
codec::Handle::new(cmd["ARRAYEDIT_SOURCE:".len()..].parse().unwrap_or(0));
|
||||||
);
|
|
||||||
let document = &self.tabs[i].scene.document;
|
let document = &self.tabs[i].scene.document;
|
||||||
let source = crate::scene::assoc_array::array_of_insert(document, handle)
|
let source = crate::scene::assoc_array::array_of_insert(document, handle).and_then(
|
||||||
.and_then(|array| {
|
|array| {
|
||||||
document
|
document
|
||||||
.block_records
|
.block_records
|
||||||
.iter()
|
.iter()
|
||||||
.find(|record| record.handle == array.source_block)
|
.find(|record| record.handle == array.source_block)
|
||||||
.map(|record| record.name.clone())
|
.map(|record| record.name.clone())
|
||||||
});
|
},
|
||||||
|
);
|
||||||
match source {
|
match source {
|
||||||
Some(name) => return Some(self.dispatch_command(&format!("BEDIT_OPEN:{name}"))),
|
Some(name) => {
|
||||||
|
return Some(self.dispatch_command(&format!("BEDIT_OPEN:{name}")))
|
||||||
|
}
|
||||||
None => self.command_line.push_error(
|
None => self.command_line.push_error(
|
||||||
crate::t!("ARRAYEDIT: the selected object is not an associative array.")
|
crate::t!("ARRAYEDIT: the selected object is not an associative array.")
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
@@ -439,8 +443,7 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard);
|
let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard);
|
||||||
use crate::modules::insert::insert_block::InsertBlockCommand;
|
use crate::modules::insert::insert_block::InsertBlockCommand;
|
||||||
let cmd =
|
let cmd = InsertBlockCommand::new_for_block(name, wires, base);
|
||||||
InsertBlockCommand::new_for_block(name, wires, base);
|
|
||||||
self.command_line.push_info(&cmd.prompt());
|
self.command_line.push_info(&cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ impl OpenCADStudio {
|
|||||||
.map(|session| session.br_handle);
|
.map(|session| session.br_handle);
|
||||||
if block.is_none() {
|
if block.is_none() {
|
||||||
self.command_line.push_error(
|
self.command_line.push_error(
|
||||||
crate::tf!("{}: open the block in the Block Editor first (BEDIT).", command)
|
crate::tf!(
|
||||||
.as_ref(),
|
"{}: open the block in the Block Editor first (BEDIT).",
|
||||||
|
command
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
block
|
block
|
||||||
@@ -29,10 +32,12 @@ impl OpenCADStudio {
|
|||||||
.block_records
|
.block_records
|
||||||
.iter()
|
.iter()
|
||||||
.find(|record| record.handle == block)
|
.find(|record| record.handle == block)
|
||||||
.and_then(|record| match document.get_entity(record.block_entity_handle) {
|
.and_then(
|
||||||
Some(EntityType::Block(begin)) => Some(begin.base_point),
|
|record| match document.get_entity(record.block_entity_handle) {
|
||||||
_ => None,
|
Some(EntityType::Block(begin)) => Some(begin.base_point),
|
||||||
})
|
_ => None,
|
||||||
|
},
|
||||||
|
)
|
||||||
.unwrap_or(codec::types::Vector3::ZERO);
|
.unwrap_or(codec::types::Vector3::ZERO);
|
||||||
visibility::create(document, block, base)
|
visibility::create(document, block, base)
|
||||||
}
|
}
|
||||||
@@ -40,10 +45,15 @@ impl OpenCADStudio {
|
|||||||
/// The state the block shows in the editor.
|
/// The state the block shows in the editor.
|
||||||
fn visibility_current(&self, i: usize, block: Handle) -> usize {
|
fn visibility_current(&self, i: usize, block: Handle) -> usize {
|
||||||
let scene = &self.tabs[i].scene;
|
let scene = &self.tabs[i].scene;
|
||||||
scene.visibility_states.get(&block).copied().unwrap_or_else(|| {
|
scene
|
||||||
visibility::parameter(&scene.document, block)
|
.visibility_states
|
||||||
.map_or(0, |parameter| visibility::shown_state(&scene.document, parameter.handle))
|
.get(&block)
|
||||||
})
|
.copied()
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
visibility::parameter(&scene.document, block).map_or(0, |parameter| {
|
||||||
|
visibility::shown_state(&scene.document, parameter.handle)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_visibility_state(&mut self, i: usize, block: Handle, parameter: Handle, state: usize) {
|
fn show_visibility_state(&mut self, i: usize, block: Handle, parameter: Handle, state: usize) {
|
||||||
@@ -51,8 +61,8 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].scene.visibility_states.insert(block, state);
|
self.tabs[i].scene.visibility_states.insert(block, state);
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.bump_geometry();
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
if let Some(name) = visibility::state_names(&self.tabs[i].scene.document, parameter)
|
if let Some(name) =
|
||||||
.get(state)
|
visibility::state_names(&self.tabs[i].scene.document, parameter).get(state)
|
||||||
{
|
{
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(crate::tf!("Current visibility state: {}", name).as_ref());
|
.push_output(crate::tf!("Current visibility state: {}", name).as_ref());
|
||||||
|
|||||||
@@ -188,7 +188,11 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::app) fn start_count_command(&mut self, i: usize, command: Box<dyn CadCommand>) -> Task<Message> {
|
pub(in crate::app) fn start_count_command(
|
||||||
|
&mut self,
|
||||||
|
i: usize,
|
||||||
|
command: Box<dyn CadCommand>,
|
||||||
|
) -> Task<Message> {
|
||||||
self.reset_command_start_state(i);
|
self.reset_command_start_state(i);
|
||||||
self.command_line.push_info(&command.prompt());
|
self.command_line.push_info(&command.prompt());
|
||||||
self.tabs[i].active_cmd = Some(command);
|
self.tabs[i].active_cmd = Some(command);
|
||||||
|
|||||||
@@ -2153,16 +2153,17 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
let doc = &self.tabs[i].scene.document;
|
let doc = &self.tabs[i].scene.document;
|
||||||
let current_style = doc.header.current_table_style_name.clone();
|
let current_style = doc.header.current_table_style_name.clone();
|
||||||
table.table_style_handle = doc.objects.iter().find_map(|(handle, object)| {
|
table.table_style_handle = doc
|
||||||
match object {
|
.objects
|
||||||
|
.iter()
|
||||||
|
.find_map(|(handle, object)| match object {
|
||||||
codec::objects::ObjectType::TableStyle(style)
|
codec::objects::ObjectType::TableStyle(style)
|
||||||
if style.name.eq_ignore_ascii_case(¤t_style) =>
|
if style.name.eq_ignore_ascii_case(¤t_style) =>
|
||||||
{
|
{
|
||||||
Some(*handle)
|
Some(*handle)
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
});
|
||||||
});
|
|
||||||
let command =
|
let command =
|
||||||
crate::modules::annotate::data_link::DataLinkPlaceCommand::new(table, link_path);
|
crate::modules::annotate::data_link::DataLinkPlaceCommand::new(table, link_path);
|
||||||
self.command_line
|
self.command_line
|
||||||
|
|||||||
@@ -443,7 +443,8 @@ impl OpenCADStudio {
|
|||||||
Ok(text) => return Some(self.run_script(clean_path, &text)),
|
Ok(text) => return Some(self.run_script(clean_path, &text)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.command_line.push_error(
|
self.command_line.push_error(
|
||||||
crate::tf!("SCRIPT: cannot read {clean_path}: {e}").as_ref(),
|
crate::tf!("SCRIPT: cannot read {clean_path}: {e}")
|
||||||
|
.as_ref(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -733,9 +734,8 @@ impl OpenCADStudio {
|
|||||||
let drawing = match result {
|
let drawing = match result {
|
||||||
Ok(bytes) => bytes,
|
Ok(bytes) => bytes,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.command_line.push_error(
|
self.command_line
|
||||||
crate::tf!("{cmd}: cannot package the drawing ({e}).").as_ref(),
|
.push_error(crate::tf!("{cmd}: cannot package the drawing ({e}).").as_ref());
|
||||||
);
|
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -752,16 +752,18 @@ impl OpenCADStudio {
|
|||||||
crate::tf!("{cmd}: packaged {copied} file(s) into {}", zip_name).as_ref(),
|
crate::tf!("{cmd}: packaged {copied} file(s) into {}", zip_name).as_ref(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(e) => self.command_line.push_error(
|
Err(e) => self
|
||||||
crate::tf!("{cmd}: cannot build the transmittal ZIP ({e}).").as_ref(),
|
.command_line
|
||||||
),
|
.push_error(crate::tf!("{cmd}: cannot build the transmittal ZIP ({e}).").as_ref()),
|
||||||
}
|
}
|
||||||
if !skipped.is_empty() {
|
if !skipped.is_empty() {
|
||||||
let n = skipped.len();
|
let n = skipped.len();
|
||||||
let names = skipped.join(", ");
|
let names = skipped.join(", ");
|
||||||
self.command_line.push_output(
|
self.command_line.push_output(
|
||||||
crate::tf!("{cmd}: skipped {n} referenced file(s) not loaded in this session: {names}")
|
crate::tf!(
|
||||||
.as_ref(),
|
"{cmd}: skipped {n} referenced file(s) not loaded in this session: {names}"
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|||||||
@@ -785,7 +785,8 @@ impl OpenCADStudio {
|
|||||||
// the command one by one, as typed.
|
// the command one by one, as typed.
|
||||||
"-VIEW" => {
|
"-VIEW" => {
|
||||||
use crate::command::CadCommand;
|
use crate::command::CadCommand;
|
||||||
let c = crate::modules::view::view_cmd::DashViewCommand::new(self.dash_view_names(i));
|
let c =
|
||||||
|
crate::modules::view::view_cmd::DashViewCommand::new(self.dash_view_names(i));
|
||||||
self.command_line.push_info(&c.prompt());
|
self.command_line.push_info(&c.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||||
}
|
}
|
||||||
@@ -926,7 +927,8 @@ impl OpenCADStudio {
|
|||||||
// Standard orientation presets: the world views -VIEW sets,
|
// Standard orientation presets: the world views -VIEW sets,
|
||||||
// with the orthographic UCS while UCSORTHO is on. These
|
// with the orthographic UCS while UCSORTHO is on. These
|
||||||
// names take precedence over a same-named saved view.
|
// names take precedence over a same-named saved view.
|
||||||
"TOP" | "FRONT" | "BACK" | "LEFT" | "RIGHT" | "BOTTOM" | "SWISO" | "SEISO" | "NEISO" | "NWISO" => {
|
"TOP" | "FRONT" | "BACK" | "LEFT" | "RIGHT" | "BOTTOM" | "SWISO" | "SEISO"
|
||||||
|
| "NEISO" | "NWISO" => {
|
||||||
return Some(self.apply_view_preset(&sub));
|
return Some(self.apply_view_preset(&sub));
|
||||||
}
|
}
|
||||||
// ISO alone is the south-west one.
|
// ISO alone is the south-west one.
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ mod pc_extract;
|
|||||||
mod pdf_dialogs;
|
mod pdf_dialogs;
|
||||||
pub(crate) mod pdf_import;
|
pub(crate) mod pdf_import;
|
||||||
mod pdf_underlay;
|
mod pdf_underlay;
|
||||||
mod refedit;
|
|
||||||
mod plotvars;
|
mod plotvars;
|
||||||
mod point_cloud;
|
mod point_cloud;
|
||||||
|
mod refedit;
|
||||||
pub(crate) mod sheet_set;
|
pub(crate) mod sheet_set;
|
||||||
mod styleprops;
|
mod styleprops;
|
||||||
mod view;
|
mod view;
|
||||||
|
|||||||
+58
-23
@@ -15,7 +15,11 @@ use std::collections::HashSet;
|
|||||||
|
|
||||||
/// `** Command not allowed, NAME already checked out for editing **`
|
/// `** Command not allowed, NAME already checked out for editing **`
|
||||||
fn already_checked_out(name: &str) -> String {
|
fn already_checked_out(name: &str) -> String {
|
||||||
crate::tf!("** Command not allowed, {} already checked out for editing **", name).into_owned()
|
crate::tf!(
|
||||||
|
"** Command not allowed, {} already checked out for editing **",
|
||||||
|
name
|
||||||
|
)
|
||||||
|
.into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
@@ -24,12 +28,15 @@ impl OpenCADStudio {
|
|||||||
"REFEDIT" | "-REFEDIT" => Some(self.start_refedit(i, cmd == "-REFEDIT")),
|
"REFEDIT" | "-REFEDIT" => Some(self.start_refedit(i, cmd == "-REFEDIT")),
|
||||||
"REFSET" | "REFSET_ADD" | "REFSET_REMOVE" => {
|
"REFSET" | "REFSET_ADD" | "REFSET_REMOVE" => {
|
||||||
if self.tabs[i].refedit_session.is_none() {
|
if self.tabs[i].refedit_session.is_none() {
|
||||||
self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
self.command_line
|
||||||
|
.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
||||||
return Some(Task::none());
|
return Some(Task::none());
|
||||||
}
|
}
|
||||||
self.command_line.push_info(
|
self.command_line.push_info(
|
||||||
crate::t!("Transfer objects between the RefEdit working set and host drawing...")
|
crate::t!(
|
||||||
.as_ref(),
|
"Transfer objects between the RefEdit working set and host drawing..."
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
);
|
);
|
||||||
// The side toolbar's Add / Remove go straight to the selection.
|
// The side toolbar's Add / Remove go straight to the selection.
|
||||||
let command = match cmd {
|
let command = match cmd {
|
||||||
@@ -44,7 +51,8 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
"REFCLOSE" => {
|
"REFCLOSE" => {
|
||||||
if self.tabs[i].refedit_session.is_none() {
|
if self.tabs[i].refedit_session.is_none() {
|
||||||
self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
self.command_line
|
||||||
|
.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
||||||
return Some(Task::none());
|
return Some(Task::none());
|
||||||
}
|
}
|
||||||
let command = RefCloseCommand::new();
|
let command = RefCloseCommand::new();
|
||||||
@@ -106,10 +114,7 @@ impl OpenCADStudio {
|
|||||||
if let [handle] = selected[..] {
|
if let [handle] = selected[..] {
|
||||||
if references.contains(&handle) {
|
if references.contains(&handle) {
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
self.refedit_picked(
|
self.refedit_picked(i, &format!("{}:{}", command_line as u8, handle.value()));
|
||||||
i,
|
|
||||||
&format!("{}:{}", command_line as u8, handle.value()),
|
|
||||||
);
|
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,7 +129,11 @@ impl OpenCADStudio {
|
|||||||
fn refedit_picked(&mut self, i: usize, args: &str) {
|
fn refedit_picked(&mut self, i: usize, args: &str) {
|
||||||
let parts: Vec<&str> = args.split(':').collect();
|
let parts: Vec<&str> = args.split(':').collect();
|
||||||
let command_line = parts.first() == Some(&"1");
|
let command_line = parts.first() == Some(&"1");
|
||||||
let Some(top) = parts.get(1).and_then(|v| v.parse::<u64>().ok()).map(Handle::new) else {
|
let Some(top) = parts
|
||||||
|
.get(1)
|
||||||
|
.and_then(|v| v.parse::<u64>().ok())
|
||||||
|
.map(Handle::new)
|
||||||
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let point = (parts.len() >= 5)
|
let point = (parts.len() >= 5)
|
||||||
@@ -178,7 +187,10 @@ impl OpenCADStudio {
|
|||||||
wires: scene.block_preview_wires(&level.block_name),
|
wires: scene.block_preview_wires(&level.block_name),
|
||||||
has_attdefs: record.is_some_and(|br| {
|
has_attdefs: record.is_some_and(|br| {
|
||||||
br.entity_handles.iter().any(|h| {
|
br.entity_handles.iter().any(|h| {
|
||||||
matches!(document.get_entity(*h), Some(EntityType::AttributeDefinition(_)))
|
matches!(
|
||||||
|
document.get_entity(*h),
|
||||||
|
Some(EntityType::AttributeDefinition(_))
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
xref_path: record
|
xref_path: record
|
||||||
@@ -202,7 +214,10 @@ impl OpenCADStudio {
|
|||||||
self.active_modal = Some(crate::app::ModalKind::RefEdit);
|
self.active_modal = Some(crate::app::ModalKind::RefEdit);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::app) fn update_refedit_dialog(&mut self, message: RefEditDialogMsg) -> Task<Message> {
|
pub(in crate::app) fn update_refedit_dialog(
|
||||||
|
&mut self,
|
||||||
|
message: RefEditDialogMsg,
|
||||||
|
) -> Task<Message> {
|
||||||
let Some(state) = self.refedit_dialog.as_mut() else {
|
let Some(state) = self.refedit_dialog.as_mut() else {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
@@ -390,9 +405,12 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].refedit_session = Some(session);
|
self.tabs[i].refedit_session = Some(session);
|
||||||
self.tabs[i].refedit_closed.clear();
|
self.tabs[i].refedit_closed.clear();
|
||||||
// Fade everything outside the working set by XFADECTL. (#136)
|
// Fade everything outside the working set by XFADECTL. (#136)
|
||||||
self.tabs[i]
|
self.tabs[i].scene.set_refedit_keep(Some(
|
||||||
.scene
|
temp_handles
|
||||||
.set_refedit_keep(Some(temp_handles.iter().copied().collect::<rustc_hash::FxHashSet<_>>()));
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.collect::<rustc_hash::FxHashSet<_>>(),
|
||||||
|
));
|
||||||
self.tabs[i].scene.refedit_lock = lock;
|
self.tabs[i].scene.refedit_lock = lock;
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
@@ -404,7 +422,8 @@ impl OpenCADStudio {
|
|||||||
/// REFSET Add / Remove on the current selection.
|
/// REFSET Add / Remove on the current selection.
|
||||||
fn apply_refset(&mut self, i: usize, add: bool) {
|
fn apply_refset(&mut self, i: usize, add: bool) {
|
||||||
let Some(mut session) = self.tabs[i].refedit_session.take() else {
|
let Some(mut session) = self.tabs[i].refedit_session.take() else {
|
||||||
self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
self.command_line
|
||||||
|
.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let picked: Vec<Handle> = self.tabs[i]
|
let picked: Vec<Handle> = self.tabs[i]
|
||||||
@@ -450,7 +469,8 @@ impl OpenCADStudio {
|
|||||||
/// REFCLOSE Save (`save`) or Discard.
|
/// REFCLOSE Save (`save`) or Discard.
|
||||||
fn close_refedit(&mut self, i: usize, save: bool) {
|
fn close_refedit(&mut self, i: usize, save: bool) {
|
||||||
let Some(session) = self.tabs[i].refedit_session.take() else {
|
let Some(session) = self.tabs[i].refedit_session.take() else {
|
||||||
self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
self.command_line
|
||||||
|
.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.push_undo_snapshot(i, "REFCLOSE");
|
self.push_undo_snapshot(i, "REFCLOSE");
|
||||||
@@ -495,8 +515,8 @@ impl OpenCADStudio {
|
|||||||
use crate::modules::draw::modify::explode::normalize_entity_for_block;
|
use crate::modules::draw::modify::explode::normalize_entity_for_block;
|
||||||
let originals: HashSet<Handle> = session.temp_handles.iter().copied().collect();
|
let originals: HashSet<Handle> = session.temp_handles.iter().copied().collect();
|
||||||
let added_count = working.iter().filter(|h| !originals.contains(h)).count();
|
let added_count = working.iter().filter(|h| !originals.contains(h)).count();
|
||||||
let removed_count = session.extracted.len()
|
let removed_count =
|
||||||
- working.iter().filter(|h| originals.contains(h)).count();
|
session.extracted.len() - working.iter().filter(|h| originals.contains(h)).count();
|
||||||
let edited: Vec<EntityType> = working
|
let edited: Vec<EntityType> = working
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
||||||
@@ -690,7 +710,10 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
crate::io::save(&source, &path)?;
|
crate::io::save(&source, &path)?;
|
||||||
let keys: rustc_hash::FxHashSet<Handle> = [session.br_handle].into_iter().collect();
|
let keys: rustc_hash::FxHashSet<Handle> = [session.br_handle].into_iter().collect();
|
||||||
self.tabs[i].xref_stat_cache.0.remove(&session.br_handle.value());
|
self.tabs[i]
|
||||||
|
.xref_stat_cache
|
||||||
|
.0
|
||||||
|
.remove(&session.br_handle.value());
|
||||||
let _ = crate::io::xref::resolve_xrefs_for_keys(
|
let _ = crate::io::xref::resolve_xrefs_for_keys(
|
||||||
&mut self.tabs[i].scene.document,
|
&mut self.tabs[i].scene.document,
|
||||||
&base_dir,
|
&base_dir,
|
||||||
@@ -766,9 +789,21 @@ impl OpenCADStudio {
|
|||||||
.layers
|
.layers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|r| (SymbolKind::Layer, r.name.clone()))
|
.map(|r| (SymbolKind::Layer, r.name.clone()))
|
||||||
.chain(deps.linetypes.iter().map(|r| (SymbolKind::Linetype, r.name.clone())))
|
.chain(
|
||||||
.chain(deps.text_styles.iter().map(|r| (SymbolKind::TextStyle, r.name.clone())))
|
deps.linetypes
|
||||||
.chain(deps.blocks.iter().map(|b| (SymbolKind::Block, b.name.clone())))
|
.iter()
|
||||||
|
.map(|r| (SymbolKind::Linetype, r.name.clone())),
|
||||||
|
)
|
||||||
|
.chain(
|
||||||
|
deps.text_styles
|
||||||
|
.iter()
|
||||||
|
.map(|r| (SymbolKind::TextStyle, r.name.clone())),
|
||||||
|
)
|
||||||
|
.chain(
|
||||||
|
deps.blocks
|
||||||
|
.iter()
|
||||||
|
.map(|b| (SymbolKind::Block, b.name.clone())),
|
||||||
|
)
|
||||||
.collect();
|
.collect();
|
||||||
let mut names = RefEditNames::default();
|
let mut names = RefEditNames::default();
|
||||||
for (kind, original) in wanted {
|
for (kind, original) in wanted {
|
||||||
|
|||||||
@@ -1464,9 +1464,18 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
if name == "LAYOUTCREATEVIEWPORT" {
|
if name == "LAYOUTCREATEVIEWPORT" {
|
||||||
// A profile switch shown and taken as ON / OFF or 1 / 0.
|
// A profile switch shown and taken as ON / OFF or 1 / 0.
|
||||||
let current = if crate::scene::layout_create_viewport() { "ON" } else { "OFF" };
|
let current = if crate::scene::layout_create_viewport() {
|
||||||
|
"ON"
|
||||||
|
} else {
|
||||||
|
"OFF"
|
||||||
|
};
|
||||||
let parsed = value.as_deref().map(|v| {
|
let parsed = value.as_deref().map(|v| {
|
||||||
match v.trim().trim_start_matches('_').to_ascii_uppercase().as_str() {
|
match v
|
||||||
|
.trim()
|
||||||
|
.trim_start_matches('_')
|
||||||
|
.to_ascii_uppercase()
|
||||||
|
.as_str()
|
||||||
|
{
|
||||||
"1" | "ON" => Some(true),
|
"1" | "ON" => Some(true),
|
||||||
"0" | "OFF" => Some(false),
|
"0" | "OFF" => Some(false),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -1483,13 +1492,15 @@ impl OpenCADStudio {
|
|||||||
self.command_line.push_error(
|
self.command_line.push_error(
|
||||||
crate::t!("Requires 0 or 1 only, or option keyword.").as_ref(),
|
crate::t!("Requires 0 or 1 only, or option keyword.").as_ref(),
|
||||||
);
|
);
|
||||||
self.command_line
|
self.command_line.push_output(&format!(
|
||||||
.push_output(&format!("Enter new value for {name} <{current}>:"));
|
"Enter new value for {name} <{current}>:"
|
||||||
|
));
|
||||||
self.pending_setvar = Some(name.clone());
|
self.pending_setvar = Some(name.clone());
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.command_line
|
self.command_line.push_output(&format!(
|
||||||
.push_output(&format!("Enter new value for {name} <{current}>:"));
|
"Enter new value for {name} <{current}>:"
|
||||||
|
));
|
||||||
self.pending_setvar = Some(name.clone());
|
self.pending_setvar = Some(name.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1497,8 +1508,15 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
if matches!(name.as_str(), "UCSORTHO" | "UCSVIEW") {
|
if matches!(name.as_str(), "UCSORTHO" | "UCSVIEW") {
|
||||||
use crate::modules::view::view_cmd;
|
use crate::modules::view::view_cmd;
|
||||||
let current = i16::from(if name == "UCSORTHO" { view_cmd::ucs_ortho() } else { view_cmd::ucs_view() });
|
let current = i16::from(if name == "UCSORTHO" {
|
||||||
match value.as_deref().map(|v| v.trim().parse::<i16>().ok().filter(|v| (0..=1).contains(v))) {
|
view_cmd::ucs_ortho()
|
||||||
|
} else {
|
||||||
|
view_cmd::ucs_view()
|
||||||
|
});
|
||||||
|
match value
|
||||||
|
.as_deref()
|
||||||
|
.map(|v| v.trim().parse::<i16>().ok().filter(|v| (0..=1).contains(v)))
|
||||||
|
{
|
||||||
Some(Some(mode)) => {
|
Some(Some(mode)) => {
|
||||||
if current != mode {
|
if current != mode {
|
||||||
if name == "UCSORTHO" {
|
if name == "UCSORTHO" {
|
||||||
@@ -1510,12 +1528,17 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(None) => {
|
Some(None) => {
|
||||||
self.command_line.push_error(crate::t!("Requires 0 or 1 only.").as_ref());
|
self.command_line
|
||||||
self.command_line.push_output(&format!("Enter new value for {name} <{current}>:"));
|
.push_error(crate::t!("Requires 0 or 1 only.").as_ref());
|
||||||
|
self.command_line.push_output(&format!(
|
||||||
|
"Enter new value for {name} <{current}>:"
|
||||||
|
));
|
||||||
self.pending_setvar = Some(name.clone());
|
self.pending_setvar = Some(name.clone());
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.command_line.push_output(&format!("Enter new value for {name} <{current}>:"));
|
self.command_line.push_output(&format!(
|
||||||
|
"Enter new value for {name} <{current}>:"
|
||||||
|
));
|
||||||
self.pending_setvar = Some(name.clone());
|
self.pending_setvar = Some(name.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3393,10 +3416,11 @@ impl OpenCADStudio {
|
|||||||
let path = std::path::PathBuf::from(cmd.trim_start_matches("COMPARE").trim());
|
let path = std::path::PathBuf::from(cmd.trim_start_matches("COMPARE").trim());
|
||||||
match crate::io::load_file(&path) {
|
match crate::io::load_file(&path) {
|
||||||
Ok(other) => {
|
Ok(other) => {
|
||||||
let comparison =
|
let comparison = crate::scene::compare::compare(&self.tabs[i].scene, other);
|
||||||
crate::scene::compare::compare(&self.tabs[i].scene, other);
|
|
||||||
// The overlay shows at once, before the cursor moves.
|
// The overlay shows at once, before the cursor moves.
|
||||||
self.tabs[i].scene.set_preview_wires(comparison.overlay.clone());
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.set_preview_wires(comparison.overlay.clone());
|
||||||
let command =
|
let command =
|
||||||
crate::modules::manage::compare::CompareCommand::new(comparison);
|
crate::modules::manage::compare::CompareCommand::new(comparison);
|
||||||
self.command_line.push_info(&command.prompt());
|
self.command_line.push_info(&command.prompt());
|
||||||
|
|||||||
@@ -589,9 +589,9 @@ impl OpenCADStudio {
|
|||||||
self.active_theme = resolved;
|
self.active_theme = resolved;
|
||||||
self.sync_model_space_theme(true);
|
self.sync_model_space_theme(true);
|
||||||
self.persist_settings_if_changed();
|
self.persist_settings_if_changed();
|
||||||
self.command_line.push_output(crate::tf!(
|
self.command_line.push_output(
|
||||||
"Color scheme set to 'System' (currently {name})."
|
crate::tf!("Color scheme set to 'System' (currently {name}).").as_ref(),
|
||||||
).as_ref());
|
);
|
||||||
return Some(Task::none());
|
return Some(Task::none());
|
||||||
}
|
}
|
||||||
let theme = crate::app::config::parse_theme_name(sub);
|
let theme = crate::app::config::parse_theme_name(sub);
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ impl OpenCADStudio {
|
|||||||
.collect();
|
.collect();
|
||||||
let name_of = |list: &[(String, Handle)], handle: Handle| {
|
let name_of = |list: &[(String, Handle)], handle: Handle| {
|
||||||
(!handle.is_null())
|
(!handle.is_null())
|
||||||
.then(|| list.iter().find(|(_, h)| *h == handle).map(|(name, _)| name.clone()))
|
.then(|| {
|
||||||
|
list.iter()
|
||||||
|
.find(|(_, h)| *h == handle)
|
||||||
|
.map(|(name, _)| name.clone())
|
||||||
|
})
|
||||||
.flatten()
|
.flatten()
|
||||||
};
|
};
|
||||||
let views = document
|
let views = document
|
||||||
@@ -57,24 +61,25 @@ impl OpenCADStudio {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let scene = &self.tabs[i].scene;
|
let scene = &self.tabs[i].scene;
|
||||||
let sheet_viewports = (scene.current_layout != "Model" && scene.active_viewport.is_none()).then(|| {
|
let sheet_viewports = (scene.current_layout != "Model" && scene.active_viewport.is_none())
|
||||||
scene
|
.then(|| {
|
||||||
.layout_content_viewports()
|
scene
|
||||||
.iter()
|
.layout_content_viewports()
|
||||||
.filter_map(|handle| match document.get_entity(*handle) {
|
.iter()
|
||||||
Some(codec::EntityType::Viewport(value)) => Some((
|
.filter_map(|handle| match document.get_entity(*handle) {
|
||||||
handle.value(),
|
Some(codec::EntityType::Viewport(value)) => Some((
|
||||||
[
|
handle.value(),
|
||||||
value.center.x - value.width / 2.0,
|
[
|
||||||
value.center.y - value.height / 2.0,
|
value.center.x - value.width / 2.0,
|
||||||
value.center.x + value.width / 2.0,
|
value.center.y - value.height / 2.0,
|
||||||
value.center.y + value.height / 2.0,
|
value.center.x + value.width / 2.0,
|
||||||
],
|
value.center.y + value.height / 2.0,
|
||||||
)),
|
],
|
||||||
_ => None,
|
)),
|
||||||
})
|
_ => None,
|
||||||
.collect()
|
})
|
||||||
});
|
.collect()
|
||||||
|
});
|
||||||
ViewNames {
|
ViewNames {
|
||||||
views,
|
views,
|
||||||
visual_styles: visual_styles.into_iter().map(|(name, _)| name).collect(),
|
visual_styles: visual_styles.into_iter().map(|(name, _)| name).collect(),
|
||||||
@@ -115,7 +120,11 @@ impl OpenCADStudio {
|
|||||||
let want = glam::Vec3::new(x, y, 1.0).normalize();
|
let want = glam::Vec3::new(x, y, 1.0).normalize();
|
||||||
(18..26)
|
(18..26)
|
||||||
.map(CubeRegion::Corner)
|
.map(CubeRegion::Corner)
|
||||||
.max_by(|a, b| a.snap_direction().dot(want).total_cmp(&b.snap_direction().dot(want)))
|
.max_by(|a, b| {
|
||||||
|
a.snap_direction()
|
||||||
|
.dot(want)
|
||||||
|
.total_cmp(&b.snap_direction().dot(want))
|
||||||
|
})
|
||||||
.expect("the cube has corners")
|
.expect("the cube has corners")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -133,7 +142,8 @@ impl OpenCADStudio {
|
|||||||
self.commit_active_ucs_change(i, "-VIEW");
|
self.commit_active_ucs_change(i, "-VIEW");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.command_line.push_output(crate::t!("Regenerating model.").as_ref());
|
self.command_line
|
||||||
|
.push_output(crate::t!("Regenerating model.").as_ref());
|
||||||
self.snap_view_preset(region)
|
self.snap_view_preset(region)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +160,12 @@ impl OpenCADStudio {
|
|||||||
"PRESET" => return self.apply_view_preset(arg),
|
"PRESET" => return self.apply_view_preset(arg),
|
||||||
"DELETE" => {
|
"DELETE" => {
|
||||||
let doomed: Vec<String> = view_cmd::matching(
|
let doomed: Vec<String> = view_cmd::matching(
|
||||||
self.tabs[i].scene.document.views.iter().map(|view| view.name.as_str()),
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.views
|
||||||
|
.iter()
|
||||||
|
.map(|view| view.name.as_str()),
|
||||||
arg,
|
arg,
|
||||||
)
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -166,7 +181,10 @@ impl OpenCADStudio {
|
|||||||
view.background_handle,
|
view.background_handle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.document.delete_layer_state(&snapshot_name(name));
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.delete_layer_state(&snapshot_name(name));
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
}
|
}
|
||||||
@@ -220,13 +238,15 @@ impl OpenCADStudio {
|
|||||||
self.commit_active_ucs_change(i, "-VIEW");
|
self.commit_active_ucs_change(i, "-VIEW");
|
||||||
}
|
}
|
||||||
if self.tabs[i].scene.active_gaze_dir().dot(before) < 0.9999 {
|
if self.tabs[i].scene.active_gaze_dir().dot(before) < 0.9999 {
|
||||||
self.command_line.push_output(crate::t!("Regenerating model.").as_ref());
|
self.command_line
|
||||||
|
.push_output(crate::t!("Regenerating model.").as_ref());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"SAVE" => {
|
"SAVE" => {
|
||||||
let mut view = self.tabs[i].scene.current_as_named_view(arg);
|
let mut view = self.tabs[i].scene.current_as_named_view(arg);
|
||||||
if view_cmd::ucs_view() {
|
if view_cmd::ucs_view() {
|
||||||
self.command_line.push_output("UCSVIEW = 1 UCS will be saved with view");
|
self.command_line
|
||||||
|
.push_output("UCSVIEW = 1 UCS will be saved with view");
|
||||||
self.store_ucs_in_view(i, &mut view);
|
self.store_ucs_in_view(i, &mut view);
|
||||||
}
|
}
|
||||||
self.save_named_view(i, view);
|
self.save_named_view(i, view);
|
||||||
@@ -237,18 +257,32 @@ impl OpenCADStudio {
|
|||||||
let (x, y) = s.split_once(',')?;
|
let (x, y) = s.split_once(',')?;
|
||||||
Some((x.trim().parse::<f64>().ok()?, y.trim().parse::<f64>().ok()?))
|
Some((x.trim().parse::<f64>().ok()?, y.trim().parse::<f64>().ok()?))
|
||||||
};
|
};
|
||||||
match (parts.first(), parts.get(1).and_then(|s| corner(s)), parts.get(2).and_then(|s| corner(s))) {
|
match (
|
||||||
|
parts.first(),
|
||||||
|
parts.get(1).and_then(|s| corner(s)),
|
||||||
|
parts.get(2).and_then(|s| corner(s)),
|
||||||
|
) {
|
||||||
(Some(name), Some(a), Some(b)) if a.0 != b.0 && a.1 != b.1 => {
|
(Some(name), Some(a), Some(b)) if a.0 != b.0 && a.1 != b.1 => {
|
||||||
// A plan view of the window between the corners; the
|
// A plan view of the window between the corners; the
|
||||||
// reference keeps no UCS with a window view.
|
// reference keeps no UCS with a window view.
|
||||||
let mut view = self.tabs[i].scene.current_as_named_view(name);
|
let mut view = self.tabs[i].scene.current_as_named_view(name);
|
||||||
view.target = Vector3 { x: (a.0 + b.0) / 2.0, y: (a.1 + b.1) / 2.0, z: 0.0 };
|
view.target = Vector3 {
|
||||||
view.direction = Vector3 { x: 0.0, y: 0.0, z: 1.0 };
|
x: (a.0 + b.0) / 2.0,
|
||||||
|
y: (a.1 + b.1) / 2.0,
|
||||||
|
z: 0.0,
|
||||||
|
};
|
||||||
|
view.direction = Vector3 {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
z: 1.0,
|
||||||
|
};
|
||||||
view.width = (b.0 - a.0).abs();
|
view.width = (b.0 - a.0).abs();
|
||||||
view.height = (b.1 - a.1).abs();
|
view.height = (b.1 - a.1).abs();
|
||||||
self.save_named_view(i, view);
|
self.save_named_view(i, view);
|
||||||
}
|
}
|
||||||
_ => self.command_line.push_output("Invalid window specification."),
|
_ => self
|
||||||
|
.command_line
|
||||||
|
.push_output("Invalid window specification."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"SETTINGS" => settings_again = true,
|
"SETTINGS" => settings_again = true,
|
||||||
@@ -307,12 +341,21 @@ impl OpenCADStudio {
|
|||||||
/// A model-space view restored in the entered paper-space viewport, with
|
/// A model-space view restored in the entered paper-space viewport, with
|
||||||
/// its background and visual style.
|
/// its background and visual style.
|
||||||
fn restore_view_in_active_viewport(&mut self, i: usize, view: &codec::tables::View) {
|
fn restore_view_in_active_viewport(&mut self, i: usize, view: &codec::tables::View) {
|
||||||
let Some(viewport) = self.tabs[i].scene.active_viewport else { return };
|
let Some(viewport) = self.tabs[i].scene.active_viewport else {
|
||||||
self.tabs[i].scene.restore_named_view_in_viewport(view, viewport);
|
return;
|
||||||
if let Some(ObjectType::VisualStyle(style)) =
|
};
|
||||||
self.tabs[i].scene.document.objects.get(&view.visual_style_handle)
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.restore_named_view_in_viewport(view, viewport);
|
||||||
|
if let Some(ObjectType::VisualStyle(style)) = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.get(&view.visual_style_handle)
|
||||||
{
|
{
|
||||||
if let Some((mode, _)) = crate::modules::view::visual_style::mode_for_style_name(&style.description) {
|
if let Some((mode, _)) =
|
||||||
|
crate::modules::view::visual_style::mode_for_style_name(&style.description)
|
||||||
|
{
|
||||||
self.tabs[i].scene.set_active_viewport_render_mode(mode);
|
self.tabs[i].scene.set_active_viewport_render_mode(mode);
|
||||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||||
}
|
}
|
||||||
@@ -368,11 +411,15 @@ impl OpenCADStudio {
|
|||||||
view.handle = document.allocate_handle();
|
view.handle = document.allocate_handle();
|
||||||
}
|
}
|
||||||
let find = |list: Vec<(String, Handle)>| {
|
let find = |list: Vec<(String, Handle)>| {
|
||||||
list.into_iter().find(|(entry, _)| entry.eq_ignore_ascii_case(value)).map_or(Handle::NULL, |(_, h)| h)
|
list.into_iter()
|
||||||
|
.find(|(entry, _)| entry.eq_ignore_ascii_case(value))
|
||||||
|
.map_or(Handle::NULL, |(_, h)| h)
|
||||||
};
|
};
|
||||||
match setting {
|
match setting {
|
||||||
"BACKGROUND" => replaced = std::mem::replace(&mut view.background_handle, Handle::NULL),
|
"BACKGROUND" => replaced = std::mem::replace(&mut view.background_handle, Handle::NULL),
|
||||||
"VISUALSTYLE" => view.visual_style_handle = find(crate::io::visual_styles::offered(document)),
|
"VISUALSTYLE" => {
|
||||||
|
view.visual_style_handle = find(crate::io::visual_styles::offered(document))
|
||||||
|
}
|
||||||
"SECTION" => {
|
"SECTION" => {
|
||||||
let sections: Vec<(String, Handle)> = document
|
let sections: Vec<(String, Handle)> = document
|
||||||
.entities()
|
.entities()
|
||||||
@@ -406,10 +453,18 @@ impl OpenCADStudio {
|
|||||||
let text = |record: &codec::objects::XRecord, code: i32| {
|
let text = |record: &codec::objects::XRecord, code: i32| {
|
||||||
record.get_string(code).unwrap_or_default().to_string()
|
record.get_string(code).unwrap_or_default().to_string()
|
||||||
};
|
};
|
||||||
let category = if setting == "CATEGORIZE" { value.to_string() } else { text(record, 300) };
|
let category = if setting == "CATEGORIZE" {
|
||||||
|
value.to_string()
|
||||||
|
} else {
|
||||||
|
text(record, 300)
|
||||||
|
};
|
||||||
let layers = layer_name.unwrap_or_else(|| text(record, 302));
|
let layers = layer_name.unwrap_or_else(|| text(record, 302));
|
||||||
let mut rest: Vec<_> =
|
let mut rest: Vec<_> = record
|
||||||
record.entries.iter().filter(|entry| !matches!(entry.code, 300 | 302)).cloned().collect();
|
.entries
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| !matches!(entry.code, 300 | 302))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
if rest.is_empty() {
|
if rest.is_empty() {
|
||||||
rest = vec![
|
rest = vec![
|
||||||
codec::objects::XRecordEntry::bool(293, false),
|
codec::objects::XRecordEntry::bool(293, false),
|
||||||
@@ -467,7 +522,10 @@ impl OpenCADStudio {
|
|||||||
canvas: [0, 1, 2].map(|k| (canvas[k].clamp(0.0, 1.0) * 255.0).round() as u8),
|
canvas: [0, 1, 2].map(|k| (canvas[k].clamp(0.0, 1.0) * 255.0).round() as u8),
|
||||||
};
|
};
|
||||||
let document = &self.tabs[i].scene.document;
|
let document = &self.tabs[i].scene.document;
|
||||||
let current = document.views.get(view).map_or(Handle::NULL, |v| v.background_handle);
|
let current = document
|
||||||
|
.views
|
||||||
|
.get(view)
|
||||||
|
.map_or(Handle::NULL, |v| v.background_handle);
|
||||||
if let Some(ObjectType::ClassObject(object)) = document.objects.get(¤t) {
|
if let Some(ObjectType::ClassObject(object)) = document.objects.get(¤t) {
|
||||||
match &object.data {
|
match &object.data {
|
||||||
D::SolidBackground(value) => state.solid = unpack(value.color),
|
D::SolidBackground(value) => state.solid = unpack(value.color),
|
||||||
@@ -490,8 +548,10 @@ impl OpenCADStudio {
|
|||||||
BgPosition::Center
|
BgPosition::Center
|
||||||
};
|
};
|
||||||
state.aspect = value.maintain_aspect_ratio;
|
state.aspect = value.maintain_aspect_ratio;
|
||||||
state.offset = [value.offset.x, value.offset.y].map(crate::ui::window::pdf_dialogs::short);
|
state.offset =
|
||||||
state.scale = [value.scale.x, value.scale.y].map(crate::ui::window::pdf_dialogs::short);
|
[value.offset.x, value.offset.y].map(crate::ui::window::pdf_dialogs::short);
|
||||||
|
state.scale =
|
||||||
|
[value.scale.x, value.scale.y].map(crate::ui::window::pdf_dialogs::short);
|
||||||
state.scale_base = [value.scale.x, value.scale.y];
|
state.scale_base = [value.scale.x, value.scale.y];
|
||||||
state.preview = crate::ui::window::pdf_dialogs::bg_preview(&value.filename);
|
state.preview = crate::ui::window::pdf_dialogs::bg_preview(&value.filename);
|
||||||
}
|
}
|
||||||
@@ -536,13 +596,23 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
M::BgStep(field, dir) => {
|
M::BgStep(field, dir) => {
|
||||||
use crate::ui::window::pdf_dialogs::{bg_step, short};
|
use crate::ui::window::pdf_dialogs::{bg_step, short};
|
||||||
let current = |text: &String, default: f64| text.trim().parse::<f64>().ok().filter(|v| v.is_finite()).unwrap_or(default);
|
let current = |text: &String, default: f64| {
|
||||||
|
text.trim()
|
||||||
|
.parse::<f64>()
|
||||||
|
.ok()
|
||||||
|
.filter(|v| v.is_finite())
|
||||||
|
.unwrap_or(default)
|
||||||
|
};
|
||||||
if field < 2 {
|
if field < 2 {
|
||||||
let k = field as usize;
|
let k = field as usize;
|
||||||
state.offset[k] = short(bg_step(field, current(&state.offset[k], 0.0), dir));
|
state.offset[k] = short(bg_step(field, current(&state.offset[k], 0.0), dir));
|
||||||
} else {
|
} else {
|
||||||
let k = field as usize - 2;
|
let k = field as usize - 2;
|
||||||
let next = short(bg_step(field, current(&state.scale[k], state.scale_base[k]), dir));
|
let next = short(bg_step(
|
||||||
|
field,
|
||||||
|
current(&state.scale[k], state.scale_base[k]),
|
||||||
|
dir,
|
||||||
|
));
|
||||||
if state.aspect {
|
if state.aspect {
|
||||||
state.scale = [next.clone(), next];
|
state.scale = [next.clone(), next];
|
||||||
} else {
|
} else {
|
||||||
@@ -629,24 +699,32 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
BgKind::Image => {
|
BgKind::Image => {
|
||||||
if !std::path::Path::new(state.image.trim()).is_file() {
|
if !std::path::Path::new(state.image.trim()).is_file() {
|
||||||
self.command_line.push_error("Invalid Background Image File!");
|
self.command_line
|
||||||
|
.push_error("Invalid Background Image File!");
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
use crate::ui::window::pdf_dialogs::{BG_OFFSET_LIMIT, BG_SCALE_LIMIT};
|
use crate::ui::window::pdf_dialogs::{BG_OFFSET_LIMIT, BG_SCALE_LIMIT};
|
||||||
let number = |text: &String| text.trim().parse::<f64>().ok().filter(|v| v.is_finite());
|
let number = |text: &String| {
|
||||||
|
text.trim().parse::<f64>().ok().filter(|v| v.is_finite())
|
||||||
|
};
|
||||||
let (Some(ox), Some(oy), Some(sx), Some(sy)) = (
|
let (Some(ox), Some(oy), Some(sx), Some(sy)) = (
|
||||||
number(&state.offset[0]),
|
number(&state.offset[0]),
|
||||||
number(&state.offset[1]),
|
number(&state.offset[1]),
|
||||||
number(&state.scale[0]),
|
number(&state.scale[0]),
|
||||||
number(&state.scale[1]),
|
number(&state.scale[1]),
|
||||||
) else {
|
) else {
|
||||||
self.command_line.push_error(crate::t!("Requires a number.").as_ref());
|
self.command_line
|
||||||
|
.push_error(crate::t!("Requires a number.").as_ref());
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
// Within the reference's slider ranges; a zero scale
|
// Within the reference's slider ranges; a zero scale
|
||||||
// is ignored there, so the image keeps its scale.
|
// is ignored there, so the image keeps its scale.
|
||||||
let scale = |value: f64, base: f64| {
|
let scale = |value: f64, base: f64| {
|
||||||
if value.abs() < 1e-9 { base } else { value.clamp(-BG_SCALE_LIMIT, BG_SCALE_LIMIT) }
|
if value.abs() < 1e-9 {
|
||||||
|
base
|
||||||
|
} else {
|
||||||
|
value.clamp(-BG_SCALE_LIMIT, BG_SCALE_LIMIT)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Some(D::ImageBackground(ImageBackground {
|
Some(D::ImageBackground(ImageBackground {
|
||||||
class_version: 1,
|
class_version: 1,
|
||||||
@@ -695,8 +773,11 @@ impl OpenCADStudio {
|
|||||||
if self.tabs[i].scene.current_layout != "Model" {
|
if self.tabs[i].scene.current_layout != "Model" {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(ObjectType::VisualStyle(style)) =
|
let Some(ObjectType::VisualStyle(style)) = self.tabs[i]
|
||||||
self.tabs[i].scene.document.objects.get(&view.visual_style_handle)
|
.scene
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.get(&view.visual_style_handle)
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -705,7 +786,11 @@ impl OpenCADStudio {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// A style of its own keeps its name; the render modes keep theirs.
|
// A style of its own keeps its name; the render modes keep theirs.
|
||||||
let label = if named { style.description.clone() } else { visual_style::label_for(mode).to_string() };
|
let label = if named {
|
||||||
|
style.description.clone()
|
||||||
|
} else {
|
||||||
|
visual_style::label_for(mode).to_string()
|
||||||
|
};
|
||||||
self.apply_render_mode(i, mode, &label);
|
self.apply_render_mode(i, mode, &label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::io::xref_model::Pathtype;
|
use crate::io::xref_model::Pathtype;
|
||||||
|
use crate::modules::insert::xattach::IMAGE_EXTENSIONS;
|
||||||
use crate::modules::insert::xattach::{
|
use crate::modules::insert::xattach::{
|
||||||
existing_reference, path_to_block_name, prepare_xref_definition, stored_path, XAttachCommand,
|
existing_reference, path_to_block_name, prepare_xref_definition, stored_path, XAttachCommand,
|
||||||
XrefAttachRequest, XrefPlacement,
|
XrefAttachRequest, XrefPlacement,
|
||||||
};
|
};
|
||||||
use crate::modules::insert::xattach::IMAGE_EXTENSIONS;
|
|
||||||
use crate::modules::insert::xref_cmd::display_path;
|
use crate::modules::insert::xref_cmd::display_path;
|
||||||
use crate::ui::window::xref_attach::{XrefAttachMsg, XrefAttachState};
|
use crate::ui::window::xref_attach::{XrefAttachMsg, XrefAttachState};
|
||||||
|
|
||||||
@@ -90,9 +90,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
// POINTCLOUDATTACH picks the file; with a file, or as
|
// POINTCLOUDATTACH picks the file; with a file, or as
|
||||||
// -POINTCLOUDATTACH, it asks on the command line.
|
// -POINTCLOUDATTACH, it asks on the command line.
|
||||||
("POINTCLOUDATTACH", true) if dialog => {
|
("POINTCLOUDATTACH", true) if dialog => Some(Task::done(Message::PointCloudAttachPick)),
|
||||||
Some(Task::done(Message::PointCloudAttachPick))
|
|
||||||
}
|
|
||||||
("POINTCLOUDATTACH" | "-POINTCLOUDATTACH", _) => {
|
("POINTCLOUDATTACH" | "-POINTCLOUDATTACH", _) => {
|
||||||
use crate::command::CadCommand;
|
use crate::command::CadCommand;
|
||||||
let command = crate::modules::insert::pc_attach::PointCloudAttachCommand::new();
|
let command = crate::modules::insert::pc_attach::PointCloudAttachCommand::new();
|
||||||
|
|||||||
+51
-18
@@ -922,7 +922,10 @@ impl OpenCADStudio {
|
|||||||
let scene = &self.tabs[self.active_tab].scene;
|
let scene = &self.tabs[self.active_tab].scene;
|
||||||
let (vw, vh) = scene.selection.borrow().view.vp_size;
|
let (vw, vh) = scene.selection.borrow().view.vp_size;
|
||||||
let (camera, bounds) = scene.viewport_edit_frame((vw, vh)).unwrap_or_else(|| {
|
let (camera, bounds) = scene.viewport_edit_frame((vw, vh)).unwrap_or_else(|| {
|
||||||
(scene.camera.borrow().clone(), scene.active_model_tile_bounds(vw, vh))
|
(
|
||||||
|
scene.camera.borrow().clone(),
|
||||||
|
scene.active_model_tile_bounds(vw, vh),
|
||||||
|
)
|
||||||
});
|
});
|
||||||
let local = iced::Rectangle {
|
let local = iced::Rectangle {
|
||||||
width: bounds.width,
|
width: bounds.width,
|
||||||
@@ -1001,7 +1004,10 @@ impl OpenCADStudio {
|
|||||||
"right" => Message::ViewportRightRelease,
|
"right" => Message::ViewportRightRelease,
|
||||||
"middle" => Message::ViewportMiddleRelease,
|
"middle" => Message::ViewportMiddleRelease,
|
||||||
_ => {
|
_ => {
|
||||||
outcome = Err(failure("invalid_request", "release is left, right or middle"));
|
outcome = Err(failure(
|
||||||
|
"invalid_request",
|
||||||
|
"release is left, right or middle",
|
||||||
|
));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1894,7 +1900,10 @@ impl OpenCADStudio {
|
|||||||
"refedit_dialog" => {
|
"refedit_dialog" => {
|
||||||
use crate::ui::window::refedit_dialog::RefEditDialogMsg as R;
|
use crate::ui::window::refedit_dialog::RefEditDialogMsg as R;
|
||||||
if self.active_modal != Some(crate::app::ModalKind::RefEdit) {
|
if self.active_modal != Some(crate::app::ModalKind::RefEdit) {
|
||||||
return Err(failure("no_dialog", "The Reference Edit dialog is not open"));
|
return Err(failure(
|
||||||
|
"no_dialog",
|
||||||
|
"The Reference Edit dialog is not open",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let spec = string(req, "value")?;
|
let spec = string(req, "value")?;
|
||||||
let (key, value) = spec.split_once('=').unwrap_or((spec, ""));
|
let (key, value) = spec.split_once('=').unwrap_or((spec, ""));
|
||||||
@@ -1906,15 +1915,17 @@ impl OpenCADStudio {
|
|||||||
if key == "cancel" {
|
if key == "cancel" {
|
||||||
Message::CloseModal
|
Message::CloseModal
|
||||||
} else {
|
} else {
|
||||||
Message::RefEditDialog(match key {
|
Message::RefEditDialog(match key {
|
||||||
"ok" => R::Ok,
|
"ok" => R::Ok,
|
||||||
"level" => R::Level(value.parse().map_err(|_| failure("invalid_value", spec))?),
|
"level" => {
|
||||||
"nested" => R::Nested(flag()?),
|
R::Level(value.parse().map_err(|_| failure("invalid_value", spec))?)
|
||||||
"attributes" => R::Attributes(flag()?),
|
}
|
||||||
"lock" => R::Lock(flag()?),
|
"nested" => R::Nested(flag()?),
|
||||||
"unique" => R::Unique(flag()?),
|
"attributes" => R::Attributes(flag()?),
|
||||||
_ => return Err(failure("invalid_value", "Unknown Reference Edit field")),
|
"lock" => R::Lock(flag()?),
|
||||||
})
|
"unique" => R::Unique(flag()?),
|
||||||
|
_ => return Err(failure("invalid_value", "Unknown Reference Edit field")),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ribbon key tips: `show`, `alt` (a lone Alt), `type:<codes>`, `enter`,
|
// Ribbon key tips: `show`, `alt` (a lone Alt), `type:<codes>`, `enter`,
|
||||||
@@ -1933,10 +1944,18 @@ impl OpenCADStudio {
|
|||||||
.chars()
|
.chars()
|
||||||
.map(|ch| K::Char(ch.to_uppercase().collect()))
|
.map(|ch| K::Char(ch.to_uppercase().collect()))
|
||||||
.collect(),
|
.collect(),
|
||||||
_ => return Err(failure("invalid_value", "Use show, alt, type:<codes>, enter, esc or hide")),
|
_ => {
|
||||||
|
return Err(failure(
|
||||||
|
"invalid_value",
|
||||||
|
"Use show, alt, type:<codes>, enter, esc or hide",
|
||||||
|
))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let tasks: Vec<_> = inputs.into_iter().map(|input| self.on_keytip(input)).collect();
|
let tasks: Vec<_> = inputs
|
||||||
|
.into_iter()
|
||||||
|
.map(|input| self.on_keytip(input))
|
||||||
|
.collect();
|
||||||
return Ok(Task::batch(tasks));
|
return Ok(Task::batch(tasks));
|
||||||
}
|
}
|
||||||
// The -VIEW Background dialog, one field per call: "type=gradient",
|
// The -VIEW Background dialog, one field per call: "type=gradient",
|
||||||
@@ -1985,9 +2004,15 @@ impl OpenCADStudio {
|
|||||||
})),
|
})),
|
||||||
"aspect" => Message::PdfDialog(P::BgAspect(value == "1")),
|
"aspect" => Message::PdfDialog(P::BgAspect(value == "1")),
|
||||||
"offset" | "scale" => {
|
"offset" | "scale" => {
|
||||||
let pair: Vec<String> = value.split(',').map(|part| part.trim().to_string()).collect();
|
let pair: Vec<String> = value
|
||||||
|
.split(',')
|
||||||
|
.map(|part| part.trim().to_string())
|
||||||
|
.collect();
|
||||||
let pair = <[String; 2]>::try_from(pair).map_err(|_| bad())?;
|
let pair = <[String; 2]>::try_from(pair).map_err(|_| bad())?;
|
||||||
if pair.iter().any(|v| v.parse::<f64>().map_or(true, |v| !v.is_finite())) {
|
if pair
|
||||||
|
.iter()
|
||||||
|
.any(|v| v.parse::<f64>().map_or(true, |v| !v.is_finite()))
|
||||||
|
{
|
||||||
return Err(bad());
|
return Err(bad());
|
||||||
}
|
}
|
||||||
if key == "offset" {
|
if key == "offset" {
|
||||||
@@ -2035,7 +2060,12 @@ impl OpenCADStudio {
|
|||||||
"apply" => Message::PdfDialog(P::IaApply),
|
"apply" => Message::PdfDialog(P::IaApply),
|
||||||
"reset" => Message::PdfDialog(P::IaReset),
|
"reset" => Message::PdfDialog(P::IaReset),
|
||||||
"brightness" | "contrast" | "fade" => {
|
"brightness" | "contrast" | "fade" => {
|
||||||
let v: u8 = value.trim().parse().ok().filter(|v| *v <= 100).ok_or_else(bad)?;
|
let v: u8 = value
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.ok()
|
||||||
|
.filter(|v| *v <= 100)
|
||||||
|
.ok_or_else(bad)?;
|
||||||
Message::PdfDialog(P::IaValue(row(key)?, v))
|
Message::PdfDialog(P::IaValue(row(key)?, v))
|
||||||
}
|
}
|
||||||
"type" => {
|
"type" => {
|
||||||
@@ -2092,7 +2122,10 @@ impl OpenCADStudio {
|
|||||||
// Choose an entry of the open multi-functional grip menu by its
|
// Choose an entry of the open multi-functional grip menu by its
|
||||||
// position (0 = first), as a click on it does.
|
// position (0 = first), as a click on it does.
|
||||||
"grip_menu_pick" => {
|
"grip_menu_pick" => {
|
||||||
let entries = self.grip_popup.as_ref().map_or(0, |popup| popup.items.len());
|
let entries = self
|
||||||
|
.grip_popup
|
||||||
|
.as_ref()
|
||||||
|
.map_or(0, |popup| popup.items.len());
|
||||||
if entries == 0 {
|
if entries == 0 {
|
||||||
return Err(failure("menu_closed", "No grip menu is open"));
|
return Err(failure("menu_closed", "No grip menu is open"));
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-35
@@ -861,7 +861,9 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"grips" => self.control_grips(),
|
"grips" => self.control_grips(),
|
||||||
"keytips" => self.ribbon.keytips_json(req["detail"].as_str() == Some("full")),
|
"keytips" => self
|
||||||
|
.ribbon
|
||||||
|
.keytips_json(req["detail"].as_str() == Some("full")),
|
||||||
"xdata_get" => self.xdata_read(&req),
|
"xdata_get" => self.xdata_read(&req),
|
||||||
"get_selection" => self.control_get_selection(),
|
"get_selection" => self.control_get_selection(),
|
||||||
"text_search" => self.control_text_search(&req).unwrap_or_else(|e| e),
|
"text_search" => self.control_text_search(&req).unwrap_or_else(|e| e),
|
||||||
@@ -1343,14 +1345,9 @@ impl OpenCADStudio {
|
|||||||
pub(super) fn control_track(&mut self, task: Task<Message>) -> Task<Message> {
|
pub(super) fn control_track(&mut self, task: Task<Message>) -> Task<Message> {
|
||||||
let track_zero_unit = self.control.pending.as_ref().is_some_and(|p| {
|
let track_zero_unit = self.control.pending.as_ref().is_some_and(|p| {
|
||||||
p.request["op"] == "run"
|
p.request["op"] == "run"
|
||||||
&& p.request["cmd"]
|
&& p.request["cmd"].as_str().is_some_and(|cmd| {
|
||||||
.as_str()
|
matches!(cmd.trim().to_ascii_uppercase().as_str(), "SAVE" | "QSAVE")
|
||||||
.is_some_and(|cmd| {
|
})
|
||||||
matches!(
|
|
||||||
cmd.trim().to_ascii_uppercase().as_str(),
|
|
||||||
"SAVE" | "QSAVE"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
// Most zero-unit tasks are `Task::none()` and have already completed
|
// Most zero-unit tasks are `Task::none()` and have already completed
|
||||||
// synchronously. SAVE/QSAVE are the deliberate exception: the command
|
// synchronously. SAVE/QSAVE are the deliberate exception: the command
|
||||||
@@ -1857,10 +1854,7 @@ mod tests {
|
|||||||
.as_nanos(),
|
.as_nanos(),
|
||||||
));
|
));
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
let saved = request(
|
let saved = request(&mut app, json!({"op":"save","path":path,"overwrite":true}));
|
||||||
&mut app,
|
|
||||||
json!({"op":"save","path":path,"overwrite":true}),
|
|
||||||
);
|
|
||||||
assert_eq!(saved["ok"], true, "{saved}");
|
assert_eq!(saved["ok"], true, "{saved}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
request(&mut app, json!({"op":"run","cmd":"LINE 0,5 10,5"}))["status"],
|
request(&mut app, json!({"op":"run","cmd":"LINE 0,5 10,5"}))["status"],
|
||||||
@@ -1897,10 +1891,7 @@ mod tests {
|
|||||||
));
|
));
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
let path = dir.join("blocked.dwg");
|
let path = dir.join("blocked.dwg");
|
||||||
let saved = request(
|
let saved = request(&mut app, json!({"op":"save","path":path,"overwrite":true}));
|
||||||
&mut app,
|
|
||||||
json!({"op":"save","path":path,"overwrite":true}),
|
|
||||||
);
|
|
||||||
assert_eq!(saved["ok"], true, "{saved}");
|
assert_eq!(saved["ok"], true, "{saved}");
|
||||||
// A directory at the target path makes the next write fail
|
// A directory at the target path makes the next write fail
|
||||||
// deterministically; the operation must settle as failed instead of
|
// deterministically; the operation must settle as failed instead of
|
||||||
@@ -1914,7 +1905,9 @@ mod tests {
|
|||||||
let qsave = request(&mut app, json!({"op":"run","cmd":"QSAVE"}));
|
let qsave = request(&mut app, json!({"op":"run","cmd":"QSAVE"}));
|
||||||
assert_eq!(qsave["status"], "failed", "{qsave}");
|
assert_eq!(qsave["status"], "failed", "{qsave}");
|
||||||
assert!(
|
assert!(
|
||||||
qsave["error"].as_str().is_some_and(|error| !error.is_empty()),
|
qsave["error"]
|
||||||
|
.as_str()
|
||||||
|
.is_some_and(|error| !error.is_empty()),
|
||||||
"{qsave}"
|
"{qsave}"
|
||||||
);
|
);
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
@@ -1938,7 +1931,10 @@ mod tests {
|
|||||||
}))
|
}))
|
||||||
.0;
|
.0;
|
||||||
assert_eq!(parked["status"], "accepted", "{parked}");
|
assert_eq!(parked["status"], "accepted", "{parked}");
|
||||||
assert!(app.control.capture_frame_pending, "capture should wait for a frame");
|
assert!(
|
||||||
|
app.control.capture_frame_pending,
|
||||||
|
"capture should wait for a frame"
|
||||||
|
);
|
||||||
let running = app
|
let running = app
|
||||||
.control_request(json!({"op":"operation","request_id":"cap-frame"}))
|
.control_request(json!({"op":"operation","request_id":"cap-frame"}))
|
||||||
.0;
|
.0;
|
||||||
@@ -1946,11 +1942,8 @@ mod tests {
|
|||||||
|
|
||||||
let _frame_task = app.on_control_capture_frame(true);
|
let _frame_task = app.on_control_capture_frame(true);
|
||||||
assert!(!app.control.capture_frame_pending);
|
assert!(!app.control.capture_frame_pending);
|
||||||
let screenshot = iced::window::Screenshot::new(
|
let screenshot =
|
||||||
vec![0u8; 4 * 4 * 4],
|
iced::window::Screenshot::new(vec![0u8; 4 * 4 * 4], iced::Size::new(4, 4), 1.0);
|
||||||
iced::Size::new(4, 4),
|
|
||||||
1.0,
|
|
||||||
);
|
|
||||||
app.control_screenshot(path.to_string_lossy().into_owned(), Some(screenshot));
|
app.control_screenshot(path.to_string_lossy().into_owned(), Some(screenshot));
|
||||||
let done = app
|
let done = app
|
||||||
.control_request(json!({"op":"operation","request_id":"cap-frame"}))
|
.control_request(json!({"op":"operation","request_id":"cap-frame"}))
|
||||||
@@ -2027,14 +2020,13 @@ mod tests {
|
|||||||
let mut app = OpenCADStudio::new_for_test();
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
request(&mut app, json!({"op":"new"}));
|
request(&mut app, json!({"op":"new"}));
|
||||||
request(&mut app, json!({"op":"run","cmd":"LINE 0,0 10,0"}));
|
request(&mut app, json!({"op":"run","cmd":"LINE 0,0 10,0"}));
|
||||||
let handle = request(&mut app, json!({"op":"query","type":"Line"}))["entities"][0]["handle"]
|
let handle = request(&mut app, json!({"op":"query","type":"Line"}))["entities"][0]
|
||||||
|
["handle"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string();
|
.to_string();
|
||||||
let before = app.control_state()["selection_revision"].as_u64().unwrap();
|
let before = app.control_state()["selection_revision"].as_u64().unwrap();
|
||||||
let selected = app.automation_op(&format!(
|
let selected = app.automation_op(&format!(r#"{{"op":"select","handles":["{handle}"]}}"#));
|
||||||
r#"{{"op":"select","handles":["{handle}"]}}"#
|
|
||||||
));
|
|
||||||
assert_eq!(selected["ok"], true, "{selected}");
|
assert_eq!(selected["ok"], true, "{selected}");
|
||||||
let state = app.control_state();
|
let state = app.control_state();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -2043,7 +2035,10 @@ mod tests {
|
|||||||
"selection revision should increment once: {state}"
|
"selection revision should increment once: {state}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
state["selection"].as_array().unwrap().contains(&json!(handle)),
|
state["selection"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.contains(&json!(handle)),
|
||||||
"{state}"
|
"{state}"
|
||||||
);
|
);
|
||||||
let events = app.control_request(json!({"op":"events"})).0;
|
let events = app.control_request(json!({"op":"events"})).0;
|
||||||
@@ -2051,7 +2046,10 @@ mod tests {
|
|||||||
assert_eq!(last["kind"], "selection", "{events}");
|
assert_eq!(last["kind"], "selection", "{events}");
|
||||||
assert_eq!(last["document_id"], state["document_id"], "{events}");
|
assert_eq!(last["document_id"], state["document_id"], "{events}");
|
||||||
assert!(
|
assert!(
|
||||||
last["selection"].as_array().unwrap().contains(&json!(handle)),
|
last["selection"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.contains(&json!(handle)),
|
||||||
"{events}"
|
"{events}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2255,7 +2253,10 @@ mod tests {
|
|||||||
fn pending_request_id_reuse_rejects_changed_payload() {
|
fn pending_request_id_reuse_rejects_changed_payload() {
|
||||||
let mut app = OpenCADStudio::new_for_test();
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
app.main_window = Some(iced::window::Id::unique());
|
app.main_window = Some(iced::window::Id::unique());
|
||||||
assert_eq!(request(&mut app, json!({"op":"new"}))["status"], "completed");
|
assert_eq!(
|
||||||
|
request(&mut app, json!({"op":"new"}))["status"],
|
||||||
|
"completed"
|
||||||
|
);
|
||||||
|
|
||||||
let pending = request(
|
let pending = request(
|
||||||
&mut app,
|
&mut app,
|
||||||
@@ -2277,10 +2278,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(changed["code"], "request_id_reused", "{changed}");
|
assert_eq!(changed["code"], "request_id_reused", "{changed}");
|
||||||
assert!(
|
assert!(
|
||||||
changed["error"]
|
changed["error"].as_str().unwrap().contains("pending-reuse"),
|
||||||
.as_str()
|
|
||||||
.unwrap()
|
|
||||||
.contains("pending-reuse"),
|
|
||||||
"{changed}"
|
"{changed}"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,10 @@ fn sysvar_batches_validate_atomically_and_undo_as_one_step() {
|
|||||||
r#"{{"protocol":1,"op":"sysvar","request_id":"sv-atomic-ok","document_id":{doc},"set":{{"ltscale":2.5,"clayer":"Walls"}}}}"#
|
r#"{{"protocol":1,"op":"sysvar","request_id":"sv-atomic-ok","document_id":{doc},"set":{{"ltscale":2.5,"clayer":"Walls"}}}}"#
|
||||||
));
|
));
|
||||||
assert_eq!(r["ok"], true, "{}", r["error"]);
|
assert_eq!(r["ok"], true, "{}", r["error"]);
|
||||||
assert_eq!(app.tabs[i].scene.document.header.current_layer_name, "Walls");
|
assert_eq!(
|
||||||
|
app.tabs[i].scene.document.header.current_layer_name,
|
||||||
|
"Walls"
|
||||||
|
);
|
||||||
assert_eq!(app.tabs[i].scene.document.header.linetype_scale, 2.5);
|
assert_eq!(app.tabs[i].scene.document.header.linetype_scale, 2.5);
|
||||||
assert_eq!(app.tabs[i].active_layer, "Walls");
|
assert_eq!(app.tabs[i].active_layer, "Walls");
|
||||||
assert_eq!(app.tabs[i].layers.current_layer, "Walls");
|
assert_eq!(app.tabs[i].layers.current_layer, "Walls");
|
||||||
@@ -165,7 +168,10 @@ fn sysvar_batches_validate_atomically_and_undo_as_one_step() {
|
|||||||
assert_eq!(r["ok"], false);
|
assert_eq!(r["ok"], false);
|
||||||
assert_eq!(r["code"], "invalid_sysvar_value");
|
assert_eq!(r["code"], "invalid_sysvar_value");
|
||||||
assert_eq!(app.tabs[i].scene.document.header.linetype_scale, 2.5);
|
assert_eq!(app.tabs[i].scene.document.header.linetype_scale, 2.5);
|
||||||
assert_eq!(app.tabs[i].scene.document.header.current_layer_name, "Walls");
|
assert_eq!(
|
||||||
|
app.tabs[i].scene.document.header.current_layer_name,
|
||||||
|
"Walls"
|
||||||
|
);
|
||||||
assert_eq!(app.tabs[i].history.undo_stack.len(), history_before + 1);
|
assert_eq!(app.tabs[i].history.undo_stack.len(), history_before + 1);
|
||||||
|
|
||||||
// One undo restores both header variables and all current-layer mirrors.
|
// One undo restores both header variables and all current-layer mirrors.
|
||||||
@@ -459,7 +465,10 @@ fn entities_create_supports_control_and_fit_point_splines() {
|
|||||||
.find(|entity| entity["layer"] == "CURVES")
|
.find(|entity| entity["layer"] == "CURVES")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(control_entity["degree"], 2, "{control_entity}");
|
assert_eq!(control_entity["degree"], 2, "{control_entity}");
|
||||||
assert_eq!(control_entity["control_points"].as_array().unwrap().len(), 3);
|
assert_eq!(
|
||||||
|
control_entity["control_points"].as_array().unwrap().len(),
|
||||||
|
3
|
||||||
|
);
|
||||||
assert_eq!(control_entity["weights"], json!([1.0, 2.0, 1.0]));
|
assert_eq!(control_entity["weights"], json!([1.0, 2.0, 1.0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ fn blockpalette_pin_toggles_autocollapse_and_close_hides() {
|
|||||||
/// One at a time: two headless renderers made in parallel crash the process.
|
/// One at a time: two headless renderers made in parallel crash the process.
|
||||||
fn with_stack(f: impl FnOnce() + Send + 'static) {
|
fn with_stack(f: impl FnOnce() + Send + 'static) {
|
||||||
static REAL_VIEW: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
static REAL_VIEW: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||||
let _one = REAL_VIEW.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
let _one = REAL_VIEW
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
.stack_size(32 * 1024 * 1024)
|
.stack_size(32 * 1024 * 1024)
|
||||||
.spawn(f)
|
.spawn(f)
|
||||||
@@ -562,7 +564,8 @@ fn docked_title_bar_double_click_floats_the_pallet_in_the_real_view() {
|
|||||||
with_stack(|| {
|
with_stack(|| {
|
||||||
use crate::ui::dock::PanelId;
|
use crate::ui::dock::PanelId;
|
||||||
let mut app = dock_app();
|
let mut app = dock_app();
|
||||||
let press = iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left));
|
let press =
|
||||||
|
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left));
|
||||||
let release = iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
|
let release = iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
|
||||||
iced::mouse::Button::Left,
|
iced::mouse::Button::Left,
|
||||||
));
|
));
|
||||||
|
|||||||
+16
-14
@@ -381,20 +381,22 @@ impl OpenCADStudio {
|
|||||||
};
|
};
|
||||||
match id {
|
match id {
|
||||||
crate::ui::dock::PanelId::Properties => tab.properties.view(width, chrome),
|
crate::ui::dock::PanelId::Properties => tab.properties.view(width, chrome),
|
||||||
crate::ui::dock::PanelId::GraphicAttributes => crate::ui::window::graphic_attributes::view(
|
crate::ui::dock::PanelId::GraphicAttributes => {
|
||||||
&self.graphic_attributes,
|
crate::ui::window::graphic_attributes::view(
|
||||||
&tab.scene,
|
&self.graphic_attributes,
|
||||||
&tab.properties,
|
&tab.scene,
|
||||||
width,
|
&tab.properties,
|
||||||
chrome,
|
width,
|
||||||
self.dock
|
chrome,
|
||||||
.location(id)
|
self.dock
|
||||||
.map_or(crate::app::config::DockSide::Right, |(side, _)| side),
|
.location(id)
|
||||||
matches!(
|
.map_or(crate::app::config::DockSide::Right, |(side, _)| side),
|
||||||
self.color_pick_target,
|
matches!(
|
||||||
Some((crate::app::ColorPickTarget::Gradient(_), _))
|
self.color_pick_target,
|
||||||
),
|
Some((crate::app::ColorPickTarget::Gradient(_), _))
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
crate::ui::dock::PanelId::BlockPalette => {
|
crate::ui::dock::PanelId::BlockPalette => {
|
||||||
crate::ui::window::block_palette::view(&self.block_palette, width, chrome)
|
crate::ui::window::block_palette::view(&self.block_palette, width, chrome)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -9,9 +9,9 @@ pub(crate) mod config;
|
|||||||
pub use automation::{export_headless, serve};
|
pub use automation::{export_headless, serve};
|
||||||
mod annotation_data;
|
mod annotation_data;
|
||||||
mod block_editor;
|
mod block_editor;
|
||||||
mod block_save_as;
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod block_editor_tests;
|
mod block_editor_tests;
|
||||||
|
mod block_save_as;
|
||||||
mod command_driver;
|
mod command_driver;
|
||||||
pub(crate) use command_driver::{copy_to_clipboard_kernel, paste_entities_kernel, PasteSpace};
|
pub(crate) use command_driver::{copy_to_clipboard_kernel, paste_entities_kernel, PasteSpace};
|
||||||
pub(crate) mod commands;
|
pub(crate) mod commands;
|
||||||
|
|||||||
+12
-3
@@ -225,7 +225,9 @@ fn subtract_bodies(
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["SECTIONCULL"] });
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
|
names: &["SECTIONCULL"]
|
||||||
|
});
|
||||||
|
|
||||||
fn entity_with_boolean_body(mut source: EntityType, body: &Body) -> Option<EntityType> {
|
fn entity_with_boolean_body(mut source: EntityType, body: &Body) -> Option<EntityType> {
|
||||||
let document = crate::scene::convert::acis_export::solid_to_sat(body)?;
|
let document = crate::scene::convert::acis_export::solid_to_sat(body)?;
|
||||||
@@ -1812,7 +1814,11 @@ impl super::OpenCADStudio {
|
|||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
self.command_line.push_output(
|
self.command_line.push_output(
|
||||||
crate::t!("SLICE: created %{count} result object(s).", count = results.len()).as_ref(),
|
crate::t!(
|
||||||
|
"SLICE: created %{count} result object(s).",
|
||||||
|
count = results.len()
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
);
|
);
|
||||||
Some(results)
|
Some(results)
|
||||||
}
|
}
|
||||||
@@ -1883,7 +1889,10 @@ impl super::OpenCADStudio {
|
|||||||
|
|
||||||
let keep_positive = if let Some(point) = keep_point {
|
let keep_positive = if let Some(point) = keep_point {
|
||||||
let tolerance = kernel::brep::operation_tolerance(
|
let tolerance = kernel::brep::operation_tolerance(
|
||||||
&handles.iter().map(|handle| &bodies[handle]).collect::<Vec<_>>(),
|
&handles
|
||||||
|
.iter()
|
||||||
|
.map(|handle| &bodies[handle])
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
let Some(distance) = tool.side(point.to_array()) else {
|
let Some(distance) = tool.side(point.to_array()) else {
|
||||||
self.command_line
|
self.command_line
|
||||||
|
|||||||
+17
-7
@@ -1157,9 +1157,7 @@ fn apply_updates(
|
|||||||
/// float would otherwise never match its `expected` value.
|
/// float would otherwise never match its `expected` value.
|
||||||
fn json_values_match(actual: &Value, expected: &Value) -> bool {
|
fn json_values_match(actual: &Value, expected: &Value) -> bool {
|
||||||
match (actual, expected) {
|
match (actual, expected) {
|
||||||
(Value::Number(a), Value::Number(b)) if a.is_f64() || b.is_f64() => {
|
(Value::Number(a), Value::Number(b)) if a.is_f64() || b.is_f64() => numbers_match(a, b),
|
||||||
numbers_match(a, b)
|
|
||||||
}
|
|
||||||
(Value::Array(a), Value::Array(b)) => {
|
(Value::Array(a), Value::Array(b)) => {
|
||||||
a.len() == b.len() && a.iter().zip(b).all(|(a, b)| json_values_match(a, b))
|
a.len() == b.len() && a.iter().zip(b).all(|(a, b)| json_values_match(a, b))
|
||||||
}
|
}
|
||||||
@@ -1675,11 +1673,23 @@ mod tests {
|
|||||||
Value::Number(number) => number,
|
Value::Number(number) => number,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
assert!(super::numbers_match(&number(json!(10)), &number(json!(10.0))));
|
assert!(super::numbers_match(
|
||||||
assert!(!super::numbers_match(&number(json!(10)), &number(json!(10.5))));
|
&number(json!(10)),
|
||||||
|
&number(json!(10.0))
|
||||||
|
));
|
||||||
|
assert!(!super::numbers_match(
|
||||||
|
&number(json!(10)),
|
||||||
|
&number(json!(10.5))
|
||||||
|
));
|
||||||
let past_precision = number(json!(9_007_199_254_740_993_u64));
|
let past_precision = number(json!(9_007_199_254_740_993_u64));
|
||||||
assert!(!super::numbers_match(&past_precision, &number(json!(9_007_199_254_740_992.0))));
|
assert!(!super::numbers_match(
|
||||||
assert!(super::numbers_match(&number(json!(-3)), &number(json!(-3.0))));
|
&past_precision,
|
||||||
|
&number(json!(9_007_199_254_740_992.0))
|
||||||
|
));
|
||||||
|
assert!(super::numbers_match(
|
||||||
|
&number(json!(-3)),
|
||||||
|
&number(json!(-3.0))
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute(app: &mut OpenCADStudio, mut request: Value, id: &str) -> Value {
|
fn execute(app: &mut OpenCADStudio, mut request: Value, id: &str) -> Value {
|
||||||
|
|||||||
+5
-2
@@ -242,7 +242,8 @@ mod tests {
|
|||||||
assert_eq!(app.active_modal, Some(ModalKind::DonationPrompt));
|
assert_eq!(app.active_modal, Some(ModalKind::DonationPrompt));
|
||||||
let _ = app.update(Message::CloseModal);
|
let _ = app.update(Message::CloseModal);
|
||||||
app.donation_prompt_version = "previous-release".to_string();
|
app.donation_prompt_version = "previous-release".to_string();
|
||||||
app.pending_startup_modals.push_back(ModalKind::DonationPrompt);
|
app.pending_startup_modals
|
||||||
|
.push_back(ModalKind::DonationPrompt);
|
||||||
let _ = app.control_request(serde_json::json!({"op":"state"}));
|
let _ = app.control_request(serde_json::json!({"op":"state"}));
|
||||||
assert!(app.pending_startup_modals.is_empty());
|
assert!(app.pending_startup_modals.is_empty());
|
||||||
app.queue_startup_prompts();
|
app.queue_startup_prompts();
|
||||||
@@ -252,7 +253,9 @@ mod tests {
|
|||||||
let mut queued = OpenCADStudio::new_for_test();
|
let mut queued = OpenCADStudio::new_for_test();
|
||||||
queued.default_assoc_prompted = true;
|
queued.default_assoc_prompted = true;
|
||||||
queued.donation_prompt_version = "previous-release".to_string();
|
queued.donation_prompt_version = "previous-release".to_string();
|
||||||
queued.pending_startup_modals.push_back(ModalKind::DonationPrompt);
|
queued
|
||||||
|
.pending_startup_modals
|
||||||
|
.push_back(ModalKind::DonationPrompt);
|
||||||
let _ = queued.control_request(serde_json::json!({"op":"state"}));
|
let _ = queued.control_request(serde_json::json!({"op":"state"}));
|
||||||
queued.queue_startup_prompts();
|
queued.queue_startup_prompts();
|
||||||
assert_ne!(queued.active_modal, Some(ModalKind::DonationPrompt));
|
assert_ne!(queued.active_modal, Some(ModalKind::DonationPrompt));
|
||||||
|
|||||||
@@ -1057,7 +1057,8 @@ impl OpenCADStudio {
|
|||||||
None => local,
|
None => local,
|
||||||
});
|
});
|
||||||
let shown = crate::entities::common::format_direction(angle);
|
let shown = crate::entities::common::format_direction(angle);
|
||||||
self.command_line.push_output(crate::tf!("Angle Override: {shown}").as_ref());
|
self.command_line
|
||||||
|
.push_output(crate::tf!("Angle Override: {shown}").as_ref());
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+34
-39
@@ -1013,7 +1013,8 @@ impl OpenCADStudio {
|
|||||||
self.cliprompt_lines = crate::app::settings::clamp_clipromptlines(s.cliprompt_lines);
|
self.cliprompt_lines = crate::app::settings::clamp_clipromptlines(s.cliprompt_lines);
|
||||||
self.command_line
|
self.command_line
|
||||||
.set_cliprompt_lines(self.cliprompt_lines.clamp(0, 50) as u8);
|
.set_cliprompt_lines(self.cliprompt_lines.clamp(0, 50) as u8);
|
||||||
self.command_line.set_opacity_percent(s.command_line_opacity);
|
self.command_line
|
||||||
|
.set_opacity_percent(s.command_line_opacity);
|
||||||
self.commandline_fade_ms =
|
self.commandline_fade_ms =
|
||||||
crate::app::settings::clamp_commandline_fade_ms(s.commandline_fade_ms);
|
crate::app::settings::clamp_commandline_fade_ms(s.commandline_fade_ms);
|
||||||
self.command_line
|
self.command_line
|
||||||
@@ -1420,7 +1421,8 @@ impl OpenCADStudio {
|
|||||||
self.show_graphic_attributes = cfg.show_graphic_attributes;
|
self.show_graphic_attributes = cfg.show_graphic_attributes;
|
||||||
self.graphic_attributes.fill_on_current_layer = cfg.graphic_fills_on_current_layer;
|
self.graphic_attributes.fill_on_current_layer = cfg.graphic_fills_on_current_layer;
|
||||||
if self.show_graphic_attributes {
|
if self.show_graphic_attributes {
|
||||||
self.dock.dock_below_properties(crate::ui::dock::PanelId::GraphicAttributes);
|
self.dock
|
||||||
|
.dock_below_properties(crate::ui::dock::PanelId::GraphicAttributes);
|
||||||
}
|
}
|
||||||
self.ribbon
|
self.ribbon
|
||||||
.set_graphic_attributes(self.show_graphic_attributes);
|
.set_graphic_attributes(self.show_graphic_attributes);
|
||||||
@@ -2201,7 +2203,9 @@ impl OpenCADStudio {
|
|||||||
let enter_viewport_task = self.tabs[i]
|
let enter_viewport_task = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.saved_entered_viewport()
|
.saved_entered_viewport()
|
||||||
.map_or_else(Task::none, |handle| Task::done(Message::EnterViewport(handle)));
|
.map_or_else(Task::none, |handle| {
|
||||||
|
Task::done(Message::EnterViewport(handle))
|
||||||
|
});
|
||||||
|
|
||||||
// The tab may have been reused for the newly opened file. Any Plot
|
// The tab may have been reused for the newly opened file. Any Plot
|
||||||
// dialog cache belongs to the previous/startup drawing and must not
|
// dialog cache belongs to the previous/startup drawing and must not
|
||||||
@@ -2347,7 +2351,10 @@ impl OpenCADStudio {
|
|||||||
let document = if worker_name == "*" {
|
let document = if worker_name == "*" {
|
||||||
crate::modules::insert::wblock::extract_entities_to_doc(&document, &handles)
|
crate::modules::insert::wblock::extract_entities_to_doc(&document, &handles)
|
||||||
} else {
|
} else {
|
||||||
crate::modules::insert::wblock::extract_block_to_doc(&document, &worker_name)
|
crate::modules::insert::wblock::extract_block_to_doc(
|
||||||
|
&document,
|
||||||
|
&worker_name,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
crate::io::save(&document, &worker_path).map_err(|e| e.to_string())
|
crate::io::save(&document, &worker_path).map_err(|e| e.to_string())
|
||||||
@@ -2377,11 +2384,7 @@ impl OpenCADStudio {
|
|||||||
// Same reporting the background path used for a failed
|
// Same reporting the background path used for a failed
|
||||||
// extraction — straight through the existing message.
|
// extraction — straight through the existing message.
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
return Task::done(Message::WblockWriteFinished(
|
return Task::done(Message::WblockWriteFinished(block_name, path, Err(error)))
|
||||||
block_name,
|
|
||||||
path,
|
|
||||||
Err(error),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let ext = path
|
let ext = path
|
||||||
@@ -2939,10 +2942,8 @@ impl OpenCADStudio {
|
|||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
crate::io::fs_access::drop_handle(tab_id);
|
crate::io::fs_access::drop_handle(tab_id);
|
||||||
self.command_line.push_error(
|
self.command_line.push_error(
|
||||||
crate::tf!(
|
crate::tf!("In-place save failed ({error}); a copy was downloaded instead")
|
||||||
"In-place save failed ({error}); a copy was downloaded instead"
|
.as_ref(),
|
||||||
)
|
|
||||||
.as_ref(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.finalize_web_save(
|
self.finalize_web_save(
|
||||||
@@ -3947,8 +3948,7 @@ impl OpenCADStudio {
|
|||||||
// updates the picked file, no dialog at all. The real
|
// updates the picked file, no dialog at all. The real
|
||||||
// name (the user may have renamed it in the picker)
|
// name (the user may have renamed it in the picker)
|
||||||
// drives the save bookkeeping.
|
// drives the save bookkeeping.
|
||||||
let picked_name =
|
let picked_name = crate::io::fs_access::handle_name(tab_id).unwrap_or(filename);
|
||||||
crate::io::fs_access::handle_name(tab_id).unwrap_or(filename);
|
|
||||||
Task::batch([
|
Task::batch([
|
||||||
close,
|
close,
|
||||||
self.web_save_capture(tab_id, picked_name, ext.to_string(), version),
|
self.web_save_capture(tab_id, picked_name, ext.to_string(), version),
|
||||||
@@ -3966,12 +3966,8 @@ impl OpenCADStudio {
|
|||||||
close,
|
close,
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async move {
|
async move {
|
||||||
crate::io::fs_access::pick_save_handle(
|
crate::io::fs_access::pick_save_handle(tab_id, &pick_suggested, ext)
|
||||||
tab_id,
|
.await
|
||||||
&pick_suggested,
|
|
||||||
ext,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
},
|
},
|
||||||
move |picked| Message::WebSaveHandlePicked {
|
move |picked| Message::WebSaveHandlePicked {
|
||||||
tab_id,
|
tab_id,
|
||||||
@@ -3985,12 +3981,10 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
// Firefox/Safari (no showSaveFilePicker): untouched legacy
|
// Firefox/Safari (no showSaveFilePicker): untouched legacy
|
||||||
// download flow.
|
// download flow.
|
||||||
crate::io::fs_access::SaveRoute::Download => {
|
crate::io::fs_access::SaveRoute::Download => Task::batch([
|
||||||
Task::batch([
|
close,
|
||||||
close,
|
self.web_save_capture(tab_id, filename, ext.to_string(), version),
|
||||||
self.web_save_capture(tab_id, filename, ext.to_string(), version),
|
]),
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4013,7 +4007,8 @@ impl OpenCADStudio {
|
|||||||
// record the intended ext, which the completion half re-checks
|
// record the intended ext, which the completion half re-checks
|
||||||
// against the retained handle before writing.
|
// against the retained handle before writing.
|
||||||
crate::io::fs_access::begin_web_save(tab_id, &ext);
|
crate::io::fs_access::begin_web_save(tab_id, &ext);
|
||||||
let bounds = crate::ui::wrap_bar::dropdown_bounds(crate::app::view::VIEWPORT_CAPTURE_BOUNDS_ID);
|
let bounds =
|
||||||
|
crate::ui::wrap_bar::dropdown_bounds(crate::app::view::VIEWPORT_CAPTURE_BOUNDS_ID);
|
||||||
let Some(bounds) = bounds else {
|
let Some(bounds) = bounds else {
|
||||||
return Task::done(Message::WebSaveScreenshot {
|
return Task::done(Message::WebSaveScreenshot {
|
||||||
tab_id,
|
tab_id,
|
||||||
@@ -4054,9 +4049,7 @@ impl OpenCADStudio {
|
|||||||
picked: Result<Option<String>, String>,
|
picked: Result<Option<String>, String>,
|
||||||
) -> Task<Message> {
|
) -> Task<Message> {
|
||||||
match picked {
|
match picked {
|
||||||
Ok(Some(picked_name)) => {
|
Ok(Some(picked_name)) => self.web_save_capture(tab_id, picked_name, ext, version),
|
||||||
self.web_save_capture(tab_id, picked_name, ext, version)
|
|
||||||
}
|
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
// The picker was the save dialog: dismissing it means no
|
// The picker was the save dialog: dismissing it means no
|
||||||
// save — the download fallback must NOT fire (that would
|
// save — the download fallback must NOT fire (that would
|
||||||
@@ -4386,9 +4379,8 @@ impl OpenCADStudio {
|
|||||||
let entries = std::mem::take(&mut self.web_autosave_pending);
|
let entries = std::mem::take(&mut self.web_autosave_pending);
|
||||||
self.web_autosave_restore_all_remaining = entries.len();
|
self.web_autosave_restore_all_remaining = entries.len();
|
||||||
self.close_active_modal();
|
self.close_active_modal();
|
||||||
self.command_line.push_info(
|
self.command_line
|
||||||
crate::tf!("Restoring {} drawing(s)…", entries.len()).as_ref(),
|
.push_info(crate::tf!("Restoring {} drawing(s)…", entries.len()).as_ref());
|
||||||
);
|
|
||||||
let reads = crate::io::web_autosave::restore_all_plan(&entries)
|
let reads = crate::io::web_autosave::restore_all_plan(&entries)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(open_name, key)| {
|
.map(|(open_name, key)| {
|
||||||
@@ -4547,7 +4539,7 @@ impl OpenCADStudio {
|
|||||||
self.sync_ribbon_layers();
|
self.sync_ribbon_layers();
|
||||||
self.sync_ribbon_styles();
|
self.sync_ribbon_styles();
|
||||||
self.sync_ribbon_from_selection();
|
self.sync_ribbon_from_selection();
|
||||||
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5573,9 +5565,9 @@ impl OpenCADStudio {
|
|||||||
background_task(
|
background_task(
|
||||||
crate::t!("Print to printer"),
|
crate::t!("Print to printer"),
|
||||||
move || {
|
move || {
|
||||||
iced::futures::executor::block_on(crate::io::print_to_printer::print_wires_with(
|
iced::futures::executor::block_on(
|
||||||
page, options,
|
crate::io::print_to_printer::print_wires_with(page, options),
|
||||||
))
|
)
|
||||||
},
|
},
|
||||||
Message::PrintResult,
|
Message::PrintResult,
|
||||||
)
|
)
|
||||||
@@ -8071,7 +8063,10 @@ mod plot_paper_tests {
|
|||||||
// and PDF drivers, so an SVG plot stores the PDF driver spelling.
|
// and PDF drivers, so an SVG plot stores the PDF driver spelling.
|
||||||
let _ = app.on_plot_dlg(PlotDlgMsg::Printer(crate::ui::window::plot::OUT_SVG.into()));
|
let _ = app.on_plot_dlg(PlotDlgMsg::Printer(crate::ui::window::plot::OUT_SVG.into()));
|
||||||
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
|
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
|
||||||
let ps = app.tabs[app.active_tab].scene.plot_settings_for("Layout1").unwrap();
|
let ps = app.tabs[app.active_tab]
|
||||||
|
.scene
|
||||||
|
.plot_settings_for("Layout1")
|
||||||
|
.unwrap();
|
||||||
assert_eq!(ps.printer_name, "DWG To PDF.pc3");
|
assert_eq!(ps.printer_name, "DWG To PDF.pc3");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,12 +118,8 @@ fn is_open_fill_boundary(entity: &EntityType) -> bool {
|
|||||||
EntityType::Polyline(polyline) => {
|
EntityType::Polyline(polyline) => {
|
||||||
!polyline.flags.is_closed() && polyline.vertices.len() >= 3
|
!polyline.flags.is_closed() && polyline.vertices.len() >= 3
|
||||||
}
|
}
|
||||||
EntityType::Polyline2D(polyline) => {
|
EntityType::Polyline2D(polyline) => !polyline.is_closed() && polyline.vertices.len() >= 3,
|
||||||
!polyline.is_closed() && polyline.vertices.len() >= 3
|
EntityType::Polyline3D(polyline) => !polyline.flags.closed && polyline.vertices.len() >= 3,
|
||||||
}
|
|
||||||
EntityType::Polyline3D(polyline) => {
|
|
||||||
!polyline.flags.closed && polyline.vertices.len() >= 3
|
|
||||||
}
|
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,11 +292,15 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
M::HatchPattern(name) => self.apply_graphic_hatch_pattern(name),
|
M::HatchPattern(name) => self.apply_graphic_hatch_pattern(name),
|
||||||
M::HatchPatternConfirm => {
|
M::HatchPatternConfirm => {
|
||||||
let name = self.graphic_attributes.hatch_editor.as_ref().and_then(|editor| {
|
let name = self
|
||||||
crate::ui::properties::filtered_hatch_patterns(&editor.search)
|
.graphic_attributes
|
||||||
.get(editor.focus)
|
.hatch_editor
|
||||||
.map(|entry| entry.name.clone())
|
.as_ref()
|
||||||
});
|
.and_then(|editor| {
|
||||||
|
crate::ui::properties::filtered_hatch_patterns(&editor.search)
|
||||||
|
.get(editor.focus)
|
||||||
|
.map(|entry| entry.name.clone())
|
||||||
|
});
|
||||||
match name {
|
match name {
|
||||||
Some(name) => self.apply_graphic_hatch_pattern(name),
|
Some(name) => self.apply_graphic_hatch_pattern(name),
|
||||||
None => Task::none(),
|
None => Task::none(),
|
||||||
@@ -322,7 +322,8 @@ impl OpenCADStudio {
|
|||||||
"" | "Y" | "YES" => self.on_graphic_fill_close_response(true),
|
"" | "Y" | "YES" => self.on_graphic_fill_close_response(true),
|
||||||
"N" | "NO" => self.on_graphic_fill_close_response(false),
|
"N" | "NO" => self.on_graphic_fill_close_response(false),
|
||||||
_ => {
|
_ => {
|
||||||
self.command_line.push_error(crate::t!("Enter Yes or No.").as_ref());
|
self.command_line
|
||||||
|
.push_error(crate::t!("Enter Yes or No.").as_ref());
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,7 +353,12 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Edit the unlocked `part` of the selection as one undo step.
|
/// Edit the unlocked `part` of the selection as one undo step.
|
||||||
fn edit_graphic_part(&mut self, part: Part, label: &str, mut edit: impl FnMut(&mut EntityType)) {
|
fn edit_graphic_part(
|
||||||
|
&mut self,
|
||||||
|
part: Part,
|
||||||
|
label: &str,
|
||||||
|
mut edit: impl FnMut(&mut EntityType),
|
||||||
|
) {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let handles = self.unlocked_graphic_handles(i, part);
|
let handles = self.unlocked_graphic_handles(i, part);
|
||||||
self.apply_property_op(i, label, &handles, |app, handle| {
|
self.apply_property_op(i, label, &handles, |app, handle| {
|
||||||
@@ -363,15 +369,18 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn has_selection(&self) -> bool {
|
fn has_selection(&self) -> bool {
|
||||||
!self.tabs[self.active_tab].scene.selected_handles_in_order().is_empty()
|
!self.tabs[self.active_tab]
|
||||||
|
.scene
|
||||||
|
.selected_handles_in_order()
|
||||||
|
.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_graphic_attributes(&self, i: usize) -> ConcreteGraphicAttributes {
|
fn current_graphic_attributes(&self, i: usize) -> ConcreteGraphicAttributes {
|
||||||
let document = &self.tabs[i].scene.document;
|
let document = &self.tabs[i].scene.document;
|
||||||
let handles = self.graphic_handles(i, Part::Both);
|
let handles = self.graphic_handles(i, Part::Both);
|
||||||
let draw_depth = self.tabs[i].scene.draw_depth_map();
|
let draw_depth = self.tabs[i].scene.draw_depth_map();
|
||||||
if let Some(entity) =
|
if let Some(entity) = palette::top_handle(&handles, &draw_depth)
|
||||||
palette::top_handle(&handles, &draw_depth).and_then(|handle| document.get_entity(handle))
|
.and_then(|handle| document.get_entity(handle))
|
||||||
{
|
{
|
||||||
let common = entity.common();
|
let common = entity.common();
|
||||||
return concrete_graphic_attributes(
|
return concrete_graphic_attributes(
|
||||||
@@ -431,12 +440,27 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
pub(super) fn on_graphic_attributes_set_all(&mut self, by_layer: bool) -> Task<Message> {
|
pub(super) fn on_graphic_attributes_set_all(&mut self, by_layer: bool) -> Task<Message> {
|
||||||
let (color, linetype, lineweight, transparency) = if by_layer {
|
let (color, linetype, lineweight, transparency) = if by_layer {
|
||||||
(AcadColor::ByLayer, "ByLayer", LineWeight::ByLayer, Transparency::BY_LAYER)
|
(
|
||||||
|
AcadColor::ByLayer,
|
||||||
|
"ByLayer",
|
||||||
|
LineWeight::ByLayer,
|
||||||
|
Transparency::BY_LAYER,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
(AcadColor::ByBlock, "ByBlock", LineWeight::ByBlock, Transparency::BY_BLOCK)
|
(
|
||||||
|
AcadColor::ByBlock,
|
||||||
|
"ByBlock",
|
||||||
|
LineWeight::ByBlock,
|
||||||
|
Transparency::BY_BLOCK,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
if !self.has_selection() {
|
if !self.has_selection() {
|
||||||
self.set_current_graphic_attributes(color, linetype.to_owned(), lineweight, transparency);
|
self.set_current_graphic_attributes(
|
||||||
|
color,
|
||||||
|
linetype.to_owned(),
|
||||||
|
lineweight,
|
||||||
|
transparency,
|
||||||
|
);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
self.edit_graphic_part(Part::Both, &crate::t!("Graphic Attributes"), |entity| {
|
self.edit_graphic_part(Part::Both, &crate::t!("Graphic Attributes"), |entity| {
|
||||||
@@ -480,18 +504,27 @@ impl OpenCADStudio {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let handles: Vec<_> = values.keys().copied().collect();
|
let handles: Vec<_> = values.keys().copied().collect();
|
||||||
self.apply_property_op(i, crate::t!("Graphic Attributes"), &handles, |app, handle| {
|
self.apply_property_op(
|
||||||
let (Some(values), Some(entity)) = (
|
i,
|
||||||
values.get(&handle),
|
crate::t!("Graphic Attributes"),
|
||||||
app.tabs[i].scene.document.get_entity_mut(handle),
|
&handles,
|
||||||
) else {
|
|app, handle| {
|
||||||
return;
|
let (Some(values), Some(entity)) = (
|
||||||
};
|
values.get(&handle),
|
||||||
crate::scene::view::dispatch::apply_color(entity, values.color);
|
app.tabs[i].scene.document.get_entity_mut(handle),
|
||||||
crate::scene::view::dispatch::apply_common_prop(entity, "linetype", &values.linetype);
|
) else {
|
||||||
crate::scene::view::dispatch::apply_line_weight(entity, values.lineweight);
|
return;
|
||||||
entity.common_mut().transparency = values.transparency;
|
};
|
||||||
});
|
crate::scene::view::dispatch::apply_color(entity, values.color);
|
||||||
|
crate::scene::view::dispatch::apply_common_prop(
|
||||||
|
entity,
|
||||||
|
"linetype",
|
||||||
|
&values.linetype,
|
||||||
|
);
|
||||||
|
crate::scene::view::dispatch::apply_line_weight(entity, values.lineweight);
|
||||||
|
entity.common_mut().transparency = values.transparency;
|
||||||
|
},
|
||||||
|
);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,8 +532,10 @@ impl OpenCADStudio {
|
|||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let name = raw_name.trim();
|
let name = raw_name.trim();
|
||||||
if !crate::scene::valid_block_name(name) {
|
if !crate::scene::valid_block_name(name) {
|
||||||
self.command_line
|
self.command_line.push_error(
|
||||||
.push_error(crate::t!("Graphic Attributes: enter a valid layer name without <>/\\\":;?*|,=`.").as_ref());
|
crate::t!("Graphic Attributes: enter a valid layer name without <>/\\\":;?*|,=`.")
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
if self.tabs[i]
|
if self.tabs[i]
|
||||||
@@ -530,8 +565,9 @@ impl OpenCADStudio {
|
|||||||
layer.line_weight = values.lineweight;
|
layer.line_weight = values.lineweight;
|
||||||
layer.transparency = values.transparency;
|
layer.transparency = values.transparency;
|
||||||
if self.tabs[i].scene.document.layers.add(layer).is_err() {
|
if self.tabs[i].scene.document.layers.add(layer).is_err() {
|
||||||
self.command_line
|
self.command_line.push_error(
|
||||||
.push_error(crate::t!("Graphic Attributes: the layer could not be created.").as_ref());
|
crate::t!("Graphic Attributes: the layer could not be created.").as_ref(),
|
||||||
|
);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
@@ -781,11 +817,17 @@ impl OpenCADStudio {
|
|||||||
let document = &self.tabs[i].scene.document;
|
let document = &self.tabs[i].scene.document;
|
||||||
let (associated, new_hatches, updated_hatches, skipped) = if value == GraphicAttribute::None
|
let (associated, new_hatches, updated_hatches, skipped) = if value == GraphicAttribute::None
|
||||||
{
|
{
|
||||||
(palette::fill_handles(document, selected), Vec::new(), Vec::new(), 0)
|
(
|
||||||
|
palette::fill_handles(document, selected),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
let Some((name, pattern)) = fill_pattern(value) else {
|
let Some((name, pattern)) = fill_pattern(value) else {
|
||||||
self.command_line
|
self.command_line.push_error(
|
||||||
.push_error(crate::t!("Graphic Attributes: no hatch pattern is available.").as_ref());
|
crate::t!("Graphic Attributes: no hatch pattern is available.").as_ref(),
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let working = if self.tabs[i].editing_model_space() {
|
let working = if self.tabs[i].editing_model_space() {
|
||||||
@@ -883,13 +925,15 @@ impl OpenCADStudio {
|
|||||||
.chain(updated_hatches.iter().map(|entity| &entity.common().handle))
|
.chain(updated_hatches.iter().map(|entity| &entity.common().handle))
|
||||||
.any(|handle| self.tabs[i].scene.is_layer_locked(*handle))
|
.any(|handle| self.tabs[i].scene.is_layer_locked(*handle))
|
||||||
{
|
{
|
||||||
self.command_line
|
self.command_line.push_error(
|
||||||
.push_error(crate::t!("Graphic Attributes: an associated fill is on a locked layer.").as_ref());
|
crate::t!("Graphic Attributes: an associated fill is on a locked layer.").as_ref(),
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if skipped == 1 {
|
if skipped == 1 {
|
||||||
self.command_line
|
self.command_line.push_info(
|
||||||
.push_info(crate::t!("Graphic Attributes: 1 unsupported object was not changed.").as_ref());
|
crate::t!("Graphic Attributes: 1 unsupported object was not changed.").as_ref(),
|
||||||
|
);
|
||||||
} else if skipped > 1 {
|
} else if skipped > 1 {
|
||||||
self.command_line.push_info(
|
self.command_line.push_info(
|
||||||
crate::tf!("Graphic Attributes: {skipped} unsupported objects were not changed.")
|
crate::tf!("Graphic Attributes: {skipped} unsupported objects were not changed.")
|
||||||
@@ -959,7 +1003,11 @@ impl OpenCADStudio {
|
|||||||
};
|
};
|
||||||
if !self.has_selection() {
|
if !self.has_selection() {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.tabs[i].scene.document.header.current_entity_linetype_scale = scale;
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.header
|
||||||
|
.current_entity_linetype_scale = scale;
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -1013,7 +1061,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
polyline.close();
|
polyline.close();
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
app.tabs[i].scene.add_entity(EntityType::LwPolyline(polyline))
|
app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.add_entity(EntityType::LwPolyline(polyline))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select(app: &mut OpenCADStudio, handles: &[Handle]) {
|
fn select(app: &mut OpenCADStudio, handles: &[Handle]) {
|
||||||
@@ -1062,13 +1112,18 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn coincident_boundaries_each_get_their_own_associative_fill() {
|
fn coincident_boundaries_each_get_their_own_associative_fill() {
|
||||||
let mut app = fresh();
|
let mut app = fresh();
|
||||||
let boundaries = [add_closed_square(&mut app, 0.0), add_closed_square(&mut app, 0.0)];
|
let boundaries = [
|
||||||
|
add_closed_square(&mut app, 0.0),
|
||||||
|
add_closed_square(&mut app, 0.0),
|
||||||
|
];
|
||||||
select(&mut app, &boundaries);
|
select(&mut app, &boundaries);
|
||||||
|
|
||||||
let _ = app.on_graphic_attribute(GraphicAttribute::Solid);
|
let _ = app.on_graphic_attribute(GraphicAttribute::Solid);
|
||||||
let fills = hatches(&app);
|
let fills = hatches(&app);
|
||||||
assert_eq!(fills.len(), 2);
|
assert_eq!(fills.len(), 2);
|
||||||
assert!(fills.iter().all(|hatch| hatch.is_solid && hatch.is_associative));
|
assert!(fills
|
||||||
|
.iter()
|
||||||
|
.all(|hatch| hatch.is_solid && hatch.is_associative));
|
||||||
assert!(fills.iter().all(|hatch| hatch
|
assert!(fills.iter().all(|hatch| hatch
|
||||||
.paths
|
.paths
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1087,7 +1142,10 @@ mod tests {
|
|||||||
fn unsupported_objects_are_skipped_while_the_rest_is_filled() {
|
fn unsupported_objects_are_skipped_while_the_rest_is_filled() {
|
||||||
let mut app = fresh();
|
let mut app = fresh();
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
let boundaries = [add_closed_square(&mut app, 0.0), add_closed_square(&mut app, 20.0)];
|
let boundaries = [
|
||||||
|
add_closed_square(&mut app, 0.0),
|
||||||
|
add_closed_square(&mut app, 20.0),
|
||||||
|
];
|
||||||
select(&mut app, &boundaries);
|
select(&mut app, &boundaries);
|
||||||
let _ = app.on_graphic_attribute(GraphicAttribute::Gradient);
|
let _ = app.on_graphic_attribute(GraphicAttribute::Gradient);
|
||||||
|
|
||||||
@@ -1104,7 +1162,10 @@ mod tests {
|
|||||||
fn editing_a_shared_gradient_is_one_undo_step() {
|
fn editing_a_shared_gradient_is_one_undo_step() {
|
||||||
let mut app = fresh();
|
let mut app = fresh();
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
let boundaries = [add_closed_square(&mut app, 0.0), add_closed_square(&mut app, 20.0)];
|
let boundaries = [
|
||||||
|
add_closed_square(&mut app, 0.0),
|
||||||
|
add_closed_square(&mut app, 20.0),
|
||||||
|
];
|
||||||
select(&mut app, &boundaries);
|
select(&mut app, &boundaries);
|
||||||
let _ = app.on_graphic_attribute(GraphicAttribute::Gradient);
|
let _ = app.on_graphic_attribute(GraphicAttribute::Gradient);
|
||||||
|
|
||||||
@@ -1152,7 +1213,11 @@ mod tests {
|
|||||||
assert_eq!(palette::line_handles(document, &[hatch]), vec![boundary]);
|
assert_eq!(palette::line_handles(document, &[hatch]), vec![boundary]);
|
||||||
assert_eq!(fill_kind(&app, hatch), GraphicAttribute::Gradient);
|
assert_eq!(fill_kind(&app, hatch), GraphicAttribute::Gradient);
|
||||||
|
|
||||||
for kind in [GraphicAttribute::Solid, GraphicAttribute::Hatch, GraphicAttribute::Gradient] {
|
for kind in [
|
||||||
|
GraphicAttribute::Solid,
|
||||||
|
GraphicAttribute::Hatch,
|
||||||
|
GraphicAttribute::Gradient,
|
||||||
|
] {
|
||||||
let _ = app.on_graphic_attribute(kind);
|
let _ = app.on_graphic_attribute(kind);
|
||||||
assert_eq!(fill_kind(&app, hatch), kind);
|
assert_eq!(fill_kind(&app, hatch), kind);
|
||||||
}
|
}
|
||||||
@@ -1240,9 +1305,11 @@ mod tests {
|
|||||||
|
|
||||||
let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
|
let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
|
||||||
assert!(app.graphic_attributes.pending_fill_close.is_some());
|
assert!(app.graphic_attributes.pending_fill_close.is_some());
|
||||||
assert!(!app.tabs[i].scene.document.entities().any(|entity| {
|
assert!(!app.tabs[i]
|
||||||
matches!(entity, EntityType::Hatch(_))
|
.scene
|
||||||
}));
|
.document
|
||||||
|
.entities()
|
||||||
|
.any(|entity| { matches!(entity, EntityType::Hatch(_)) }));
|
||||||
|
|
||||||
let _ = app.update(Message::CommandOptionPick("Y".to_string()));
|
let _ = app.update(Message::CommandOptionPick("Y".to_string()));
|
||||||
assert!(app.graphic_attributes.pending_fill_close.is_none());
|
assert!(app.graphic_attributes.pending_fill_close.is_none());
|
||||||
@@ -1268,10 +1335,7 @@ mod tests {
|
|||||||
let _ = app.on_line_transparency(codec::types::Transparency::from_percent(0.3));
|
let _ = app.on_line_transparency(codec::types::Transparency::from_percent(0.3));
|
||||||
|
|
||||||
let header = &app.tabs[i].scene.document.header;
|
let header = &app.tabs[i].scene.document.header;
|
||||||
assert_eq!(
|
assert_eq!(header.current_entity_color, codec::types::Color::Index(2));
|
||||||
header.current_entity_color,
|
|
||||||
codec::types::Color::Index(2)
|
|
||||||
);
|
|
||||||
assert_eq!(header.current_linetype_name, "Continuous");
|
assert_eq!(header.current_linetype_name, "Continuous");
|
||||||
assert_eq!(header.current_line_weight, 35);
|
assert_eq!(header.current_line_weight, 35);
|
||||||
assert!((header.current_entity_linetype_scale - 2.5).abs() < f64::EPSILON);
|
assert!((header.current_entity_linetype_scale - 2.5).abs() < f64::EPSILON);
|
||||||
@@ -1606,14 +1670,18 @@ mod tests {
|
|||||||
assert_eq!(app.dock.location(PanelId::GraphicAttributes), None);
|
assert_eq!(app.dock.location(PanelId::GraphicAttributes), None);
|
||||||
|
|
||||||
let _ = app.update(Message::ToggleGraphicAttributes);
|
let _ = app.update(Message::ToggleGraphicAttributes);
|
||||||
let properties = app.dock.location(PanelId::Properties).expect("properties docked");
|
let properties = app
|
||||||
|
.dock
|
||||||
|
.location(PanelId::Properties)
|
||||||
|
.expect("properties docked");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.dock.location(PanelId::GraphicAttributes),
|
app.dock.location(PanelId::GraphicAttributes),
|
||||||
Some(properties)
|
Some(properties)
|
||||||
);
|
);
|
||||||
|
|
||||||
// A place the user chose is kept when the palette is shown again.
|
// A place the user chose is kept when the palette is shown again.
|
||||||
app.dock.dock(PanelId::GraphicAttributes, DockSide::Right, 0);
|
app.dock
|
||||||
|
.dock(PanelId::GraphicAttributes, DockSide::Right, 0);
|
||||||
let _ = app.update(Message::ToggleGraphicAttributes);
|
let _ = app.update(Message::ToggleGraphicAttributes);
|
||||||
let _ = app.update(Message::ToggleGraphicAttributes);
|
let _ = app.update(Message::ToggleGraphicAttributes);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1659,7 +1727,9 @@ mod tests {
|
|||||||
assert_eq!(common(&app, line).linetype_scale, 1.0);
|
assert_eq!(common(&app, line).linetype_scale, 1.0);
|
||||||
|
|
||||||
// Typing alone does not apply; Enter does, with a decimal comma too.
|
// Typing alone does not apply; Enter does, with a decimal comma too.
|
||||||
let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScaleInput("0,35".into())));
|
let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScaleInput(
|
||||||
|
"0,35".into(),
|
||||||
|
)));
|
||||||
assert_eq!(common(&app, line).linetype_scale, 1.0);
|
assert_eq!(common(&app, line).linetype_scale, 1.0);
|
||||||
let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScaleSubmit));
|
let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScaleSubmit));
|
||||||
assert_eq!(common(&app, line).linetype_scale, 0.35);
|
assert_eq!(common(&app, line).linetype_scale, 0.35);
|
||||||
@@ -1667,7 +1737,9 @@ mod tests {
|
|||||||
|
|
||||||
// Invalid or non-positive input is dropped.
|
// Invalid or non-positive input is dropped.
|
||||||
for typed in ["abc", "0", "-2"] {
|
for typed in ["abc", "0", "-2"] {
|
||||||
let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScaleInput(typed.into())));
|
let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScaleInput(
|
||||||
|
typed.into(),
|
||||||
|
)));
|
||||||
let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScaleSubmit));
|
let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScaleSubmit));
|
||||||
assert_eq!(common(&app, line).linetype_scale, 0.35);
|
assert_eq!(common(&app, line).linetype_scale, 0.35);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -862,11 +862,13 @@ impl OpenCADStudio {
|
|||||||
if pending.is_empty() {
|
if pending.is_empty() {
|
||||||
return autosave;
|
return autosave;
|
||||||
}
|
}
|
||||||
Task::batch(std::iter::once(autosave).chain(pending.into_iter().map(|script| {
|
Task::batch(
|
||||||
Task::perform(crate::scene::text::web_font::fetch(script), move |res| {
|
std::iter::once(autosave).chain(pending.into_iter().map(|script| {
|
||||||
Message::WebFontLoaded(script, res)
|
Task::perform(crate::scene::text::web_font::fetch(script), move |res| {
|
||||||
})
|
Message::WebFontLoaded(script, res)
|
||||||
})))
|
})
|
||||||
|
})),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web: a per-script font arrived. The same bytes feed drawing text,
|
// Web: a per-script font arrived. The same bytes feed drawing text,
|
||||||
|
|||||||
@@ -1171,7 +1171,9 @@ impl OpenCADStudio {
|
|||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
rgb.map(|rgb| {
|
rgb.map(|rgb| {
|
||||||
Message::PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg::BgColor(slot, rgb))
|
Message::PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg::BgColor(
|
||||||
|
slot, rgb,
|
||||||
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Some(crate::app::ColorPickTarget::GraphicAttributesLine) => Some(
|
Some(crate::app::ColorPickTarget::GraphicAttributesLine) => Some(
|
||||||
|
|||||||
+28
-12
@@ -1088,9 +1088,9 @@ impl OpenCADStudio {
|
|||||||
// viewport, the picker drives that viewport entity's own
|
// viewport, the picker drives that viewport entity's own
|
||||||
// render mode; the model-layout tab style is untouched.
|
// render mode; the model-layout tab style is untouched.
|
||||||
if self.tabs[i].scene.set_active_viewport_render_mode(mode) {
|
if self.tabs[i].scene.set_active_viewport_render_mode(mode) {
|
||||||
self.tabs[i]
|
self.tabs[i].scene.set_active_visual_style_named(
|
||||||
.scene
|
crate::modules::view::visual_style::style_name_for(mode),
|
||||||
.set_active_visual_style_named(crate::modules::view::visual_style::style_name_for(mode));
|
);
|
||||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.command_line
|
self.command_line
|
||||||
@@ -1099,9 +1099,9 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
self.apply_render_mode(i, mode, label);
|
self.apply_render_mode(i, mode, label);
|
||||||
// The viewport record carries the style the mode is saved as.
|
// The viewport record carries the style the mode is saved as.
|
||||||
self.tabs[i]
|
self.tabs[i].scene.set_active_visual_style_named(
|
||||||
.scene
|
crate::modules::view::visual_style::style_name_for(mode),
|
||||||
.set_active_visual_style_named(crate::modules::view::visual_style::style_name_for(mode));
|
);
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(crate::tf!("Visual style: {label}").as_ref());
|
.push_output(crate::tf!("Visual style: {label}").as_ref());
|
||||||
// The point cloud move gizmo follows the visual style.
|
// The point cloud move gizmo follows the visual style.
|
||||||
@@ -3597,8 +3597,14 @@ impl OpenCADStudio {
|
|||||||
let aperture = crate::ui::overlay::pick_box_aperture_px(self.pick_box);
|
let aperture = crate::ui::overlay::pick_box_aperture_px(self.pick_box);
|
||||||
let world = self.cursor_model_point(i, edit_cam, p, bounds);
|
let world = self.cursor_model_point(i, edit_cam, p, bounds);
|
||||||
let scene = &self.tabs[i].scene;
|
let scene = &self.tabs[i].scene;
|
||||||
let candidates = scene
|
let candidates = scene.interaction_hover_candidates_near(
|
||||||
.interaction_hover_candidates_near(wires, world, view_rot, eye, bounds, aperture * 2.0);
|
wires,
|
||||||
|
world,
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
aperture * 2.0,
|
||||||
|
);
|
||||||
scene::pick::hit_test::click_hit(
|
scene::pick::hit_test::click_hit(
|
||||||
p,
|
p,
|
||||||
&candidates,
|
&candidates,
|
||||||
@@ -5983,8 +5989,12 @@ properties={:.1}ms picked={}",
|
|||||||
// Zoom about the fingers once the drawing area's place in
|
// Zoom about the fingers once the drawing area's place in
|
||||||
// the window is known; until then about the first finger.
|
// the window is known; until then about the first finger.
|
||||||
if let Some(offset) = self.touch_offset {
|
if let Some(offset) = self.touch_offset {
|
||||||
self.tabs[i].scene.selection.borrow_mut().input.last_move_pos =
|
self.tabs[i]
|
||||||
Some(centre - offset);
|
.scene
|
||||||
|
.selection
|
||||||
|
.borrow_mut()
|
||||||
|
.input
|
||||||
|
.last_move_pos = Some(centre - offset);
|
||||||
}
|
}
|
||||||
tasks.push(self.zoom_view_at_cursor(pinch_zoom_steps(factor - 1.0)));
|
tasks.push(self.zoom_view_at_cursor(pinch_zoom_steps(factor - 1.0)));
|
||||||
}
|
}
|
||||||
@@ -6241,7 +6251,12 @@ properties={:.1}ms picked={}",
|
|||||||
self.snap_view_region_as(region, glam::Mat4::IDENTITY, false)
|
self.snap_view_region_as(region, glam::Mat4::IDENTITY, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snap_view_region_as(&mut self, region: CubeRegion, r_ucs: glam::Mat4, cube: bool) -> Task<Message> {
|
fn snap_view_region_as(
|
||||||
|
&mut self,
|
||||||
|
region: CubeRegion,
|
||||||
|
r_ucs: glam::Mat4,
|
||||||
|
cube: bool,
|
||||||
|
) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.clear_navigation_hover(i);
|
self.clear_navigation_hover(i);
|
||||||
self.tabs[i].scene.remember_current_view();
|
self.tabs[i].scene.remember_current_view();
|
||||||
@@ -6807,7 +6822,8 @@ properties={:.1}ms picked={}",
|
|||||||
// LAYOUTCREATEVIEWPORT is off.
|
// LAYOUTCREATEVIEWPORT is off.
|
||||||
if crate::scene::layout_create_viewport() {
|
if crate::scene::layout_create_viewport() {
|
||||||
let name = new_name.clone();
|
let name = new_name.clone();
|
||||||
self.command_line.push_output(crate::tf!("Layout '{name}' created.").as_ref());
|
self.command_line
|
||||||
|
.push_output(crate::tf!("Layout '{name}' created.").as_ref());
|
||||||
} else {
|
} else {
|
||||||
self.command_line.push_output(
|
self.command_line.push_output(
|
||||||
crate::tf!("Layout \"{new_name}\" created — use MVIEW to add a viewport")
|
crate::tf!("Layout \"{new_name}\" created — use MVIEW to add a viewport")
|
||||||
|
|||||||
@@ -72,10 +72,19 @@ impl OpenCADStudio {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let typed = token.trim().trim_start_matches('_').to_uppercase();
|
let typed = token.trim().trim_start_matches('_').to_uppercase();
|
||||||
let offered = self.tabs[i].active_cmd.as_ref()?.options().iter().any(|option| {
|
let offered = self.tabs[i]
|
||||||
option.keyword.eq_ignore_ascii_case(&typed)
|
.active_cmd
|
||||||
|| option.label.to_uppercase().replace(' ', "").starts_with(&typed)
|
.as_ref()?
|
||||||
});
|
.options()
|
||||||
|
.iter()
|
||||||
|
.any(|option| {
|
||||||
|
option.keyword.eq_ignore_ascii_case(&typed)
|
||||||
|
|| option
|
||||||
|
.label
|
||||||
|
.to_uppercase()
|
||||||
|
.replace(' ', "")
|
||||||
|
.starts_with(&typed)
|
||||||
|
});
|
||||||
(!offered).then_some(kind)
|
(!offered).then_some(kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -3970,7 +3970,9 @@ pub(super) fn recent_files_panel<'a>(
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
container(text(tip).size(11)).padding([2, 6]).style(container::rounded_box),
|
container(text(tip).size(11))
|
||||||
|
.padding([2, 6])
|
||||||
|
.style(container::rounded_box),
|
||||||
iced::widget::tooltip::Position::Bottom,
|
iced::widget::tooltip::Position::Bottom,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -4007,12 +4009,10 @@ pub(super) fn recent_files_panel<'a>(
|
|||||||
.into()
|
.into()
|
||||||
} else if view != RecentView::List {
|
} else if view != RecentView::List {
|
||||||
iced::widget::scrollable(
|
iced::widget::scrollable(
|
||||||
recent_tiles(recents, thumbs, view == RecentView::Preview).padding(
|
recent_tiles(recents, thumbs, view == RecentView::Preview).padding(iced::Padding {
|
||||||
iced::Padding {
|
right: 12.0,
|
||||||
right: 12.0,
|
..iced::Padding::ZERO
|
||||||
..iced::Padding::ZERO
|
}),
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
|
|||||||
+11
-8
@@ -38,7 +38,9 @@ impl OpenCADStudio {
|
|||||||
Some(K::ViewBackground) => format!(
|
Some(K::ViewBackground) => format!(
|
||||||
"{} — {}",
|
"{} — {}",
|
||||||
crate::tr!("view-background", "title"),
|
crate::tr!("view-background", "title"),
|
||||||
self.view_background.as_ref().map_or("", |s| s.view.as_str())
|
self.view_background
|
||||||
|
.as_ref()
|
||||||
|
.map_or("", |s| s.view.as_str())
|
||||||
),
|
),
|
||||||
Some(K::ImageAdjust) => crate::tr!("image-adjust", "title"),
|
Some(K::ImageAdjust) => crate::tr!("image-adjust", "title"),
|
||||||
Some(K::RefEdit) => crate::tr!("ref-edit", "dialog-title"),
|
Some(K::RefEdit) => crate::tr!("ref-edit", "dialog-title"),
|
||||||
@@ -2173,13 +2175,14 @@ fn autosave_restore_dialog_window(
|
|||||||
.size(14)]
|
.size(14)]
|
||||||
.spacing(0);
|
.spacing(0);
|
||||||
if let Some(count) = session_drawings {
|
if let Some(count) = session_drawings {
|
||||||
body = body
|
body = body.push(iced::widget::Space::new().height(4)).push(
|
||||||
.push(iced::widget::Space::new().height(4))
|
text(crate::tf!(
|
||||||
.push(
|
"Session had {count} open drawing(s).",
|
||||||
text(crate::tf!("Session had {count} open drawing(s).", count = count))
|
count = count
|
||||||
.size(12)
|
))
|
||||||
.style(dialog_muted_text_style),
|
.size(12)
|
||||||
);
|
.style(dialog_muted_text_style),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
body = body
|
body = body
|
||||||
.push(iced::widget::Space::new().height(4))
|
.push(iced::widget::Space::new().height(4))
|
||||||
|
|||||||
@@ -1794,14 +1794,14 @@ fn qselect_content<'a>(
|
|||||||
iced::widget::pick_list(Some(type_sel), type_options, |value| {
|
iced::widget::pick_list(Some(type_sel), type_options, |value| {
|
||||||
qselect_type_label(value)
|
qselect_type_label(value)
|
||||||
})
|
})
|
||||||
.on_select(|s: String| {
|
.on_select(|s: String| {
|
||||||
if s == QSELECT_ANY_TYPE {
|
if s == QSELECT_ANY_TYPE {
|
||||||
Message::QSelectSetType(None)
|
Message::QSelectSetType(None)
|
||||||
} else {
|
} else {
|
||||||
Message::QSelectSetType(Some(s))
|
Message::QSelectSetType(Some(s))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.width(field_width),
|
.width(field_width),
|
||||||
]
|
]
|
||||||
.align_y(iced::Alignment::Center)
|
.align_y(iced::Alignment::Center)
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
|
|||||||
@@ -191,9 +191,9 @@ impl OpenCADStudio {
|
|||||||
let direct = {
|
let direct = {
|
||||||
let doc = &self.tabs[i].scene.document;
|
let doc = &self.tabs[i].scene.document;
|
||||||
matches!(doc.get_entity(insert_handle), Some(EntityType::Insert(ins))
|
matches!(doc.get_entity(insert_handle), Some(EntityType::Insert(ins))
|
||||||
if doc.block_records.get(&ins.block_name).is_some_and(|record| {
|
if doc.block_records.get(&ins.block_name).is_some_and(|record| {
|
||||||
crate::scene::block_visibility::parameter(doc, record.handle).is_some()
|
crate::scene::block_visibility::parameter(doc, record.handle).is_some()
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
if direct {
|
if direct {
|
||||||
self.push_undo_snapshot(i, "Visibility State");
|
self.push_undo_snapshot(i, "Visibility State");
|
||||||
|
|||||||
@@ -5394,10 +5394,18 @@ fn append_linear_dimension(
|
|||||||
let beyond_d2 = chase_text.then_some(text_beyond_d2).flatten();
|
let beyond_d2 = chase_text.then_some(text_beyond_d2).flatten();
|
||||||
if (arrows_outside && !params.dimsoxd) || beyond_d1.is_some() || beyond_d2.is_some() {
|
if (arrows_outside && !params.dimsoxd) || beyond_d1.is_some() || beyond_d2.is_some() {
|
||||||
if !suppress.dim1 && (arrows_outside || beyond_d1.is_some()) {
|
if !suppress.dim1 && (arrows_outside || beyond_d1.is_some()) {
|
||||||
add_segment(&mut g.dim_lines, d1 - dir_d1_to_d2 * reach(stub, beyond_d1), d1);
|
add_segment(
|
||||||
|
&mut g.dim_lines,
|
||||||
|
d1 - dir_d1_to_d2 * reach(stub, beyond_d1),
|
||||||
|
d1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if !suppress.dim2 && (arrows_outside || beyond_d2.is_some()) {
|
if !suppress.dim2 && (arrows_outside || beyond_d2.is_some()) {
|
||||||
add_segment(&mut g.dim_lines, d2, d2 + dir_d1_to_d2 * reach(stub, beyond_d2));
|
add_segment(
|
||||||
|
&mut g.dim_lines,
|
||||||
|
d2,
|
||||||
|
d2 + dir_d1_to_d2 * reach(stub, beyond_d2),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1973,9 +1973,11 @@ fn grips(entity: &ExtendedEntity) -> Vec<GripDef> {
|
|||||||
};
|
};
|
||||||
// A section volume is also pushed or pulled at its top and bottom.
|
// A section volume is also pushed or pulled at its top and bottom.
|
||||||
if data.state == 4 {
|
if data.state == 4 {
|
||||||
grips.extend(section_plane_edge_grips(data).into_iter().filter(|grip| {
|
grips.extend(
|
||||||
matches!(grip.id, SECTION_GRIP_TOP | SECTION_GRIP_BOTTOM)
|
section_plane_edge_grips(data)
|
||||||
}));
|
.into_iter()
|
||||||
|
.filter(|grip| matches!(grip.id, SECTION_GRIP_TOP | SECTION_GRIP_BOTTOM)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(right) = data.vertices.last() {
|
if let Some(right) = data.vertices.last() {
|
||||||
let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z);
|
let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z);
|
||||||
|
|||||||
+57
-15
@@ -836,9 +836,9 @@ impl Grippable for Hatch {
|
|||||||
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
||||||
if let Some(point) = vertex_point_at(self, grip_id) {
|
if let Some(point) = vertex_point_at(self, grip_id) {
|
||||||
let removable = match point {
|
let removable = match point {
|
||||||
BoundaryPoint::LineVertex { ending, starting, .. } => {
|
BoundaryPoint::LineVertex {
|
||||||
ending.is_some() && starting.is_some()
|
ending, starting, ..
|
||||||
}
|
} => ending.is_some() && starting.is_some(),
|
||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
let mut items = vec![
|
let mut items = vec![
|
||||||
@@ -1009,13 +1009,32 @@ impl FallbackTess for Hatch {
|
|||||||
/// Where one boundary grip's point lives.
|
/// Where one boundary grip's point lives.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
enum BoundaryPoint {
|
enum BoundaryPoint {
|
||||||
PolylineVertex { path: usize, edge: usize, vertex: usize },
|
PolylineVertex {
|
||||||
|
path: usize,
|
||||||
|
edge: usize,
|
||||||
|
vertex: usize,
|
||||||
|
},
|
||||||
/// A corner of a chain of line edges: the end of `ending` and the start
|
/// A corner of a chain of line edges: the end of `ending` and the start
|
||||||
/// of `starting`, kept together so moving it never opens the boundary.
|
/// of `starting`, kept together so moving it never opens the boundary.
|
||||||
LineVertex { path: usize, ending: Option<usize>, starting: Option<usize> },
|
LineVertex {
|
||||||
ArcCenter { path: usize, edge: usize },
|
path: usize,
|
||||||
EllipseCenter { path: usize, edge: usize },
|
ending: Option<usize>,
|
||||||
SplinePoint { path: usize, edge: usize, index: usize, fit: bool },
|
starting: Option<usize>,
|
||||||
|
},
|
||||||
|
ArcCenter {
|
||||||
|
path: usize,
|
||||||
|
edge: usize,
|
||||||
|
},
|
||||||
|
EllipseCenter {
|
||||||
|
path: usize,
|
||||||
|
edge: usize,
|
||||||
|
},
|
||||||
|
SplinePoint {
|
||||||
|
path: usize,
|
||||||
|
edge: usize,
|
||||||
|
index: usize,
|
||||||
|
fit: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn same_point(a: &codec::types::Vector2, b: &codec::types::Vector2) -> bool {
|
fn same_point(a: &codec::types::Vector2, b: &codec::types::Vector2) -> bool {
|
||||||
@@ -1139,9 +1158,17 @@ fn boundary_points(hatch: &Hatch) -> Vec<(BoundaryPoint, [f64; 2])> {
|
|||||||
if let Some(last) = pending_end {
|
if let Some(last) = pending_end {
|
||||||
let end = line(last).unwrap().end.clone();
|
let end = line(last).unwrap().end.clone();
|
||||||
match first_start.map(|slot| (slot, &mut out[slot])) {
|
match first_start.map(|slot| (slot, &mut out[slot])) {
|
||||||
Some((_, (BoundaryPoint::LineVertex { ending, starting: Some(0), .. }, _)))
|
Some((
|
||||||
if ending.is_none() && same_point(&line(0).unwrap().start, &end) =>
|
_,
|
||||||
{
|
(
|
||||||
|
BoundaryPoint::LineVertex {
|
||||||
|
ending,
|
||||||
|
starting: Some(0),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
_,
|
||||||
|
),
|
||||||
|
)) if ending.is_none() && same_point(&line(0).unwrap().start, &end) => {
|
||||||
*ending = Some(last);
|
*ending = Some(last);
|
||||||
}
|
}
|
||||||
_ => out.push((
|
_ => out.push((
|
||||||
@@ -1167,7 +1194,11 @@ fn set_boundary_point(hatch: &mut Hatch, point: BoundaryPoint, x: f64, y: f64) {
|
|||||||
polyline.vertices[vertex].y = y;
|
polyline.vertices[vertex].y = y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BoundaryPoint::LineVertex { path, ending, starting } => {
|
BoundaryPoint::LineVertex {
|
||||||
|
path,
|
||||||
|
ending,
|
||||||
|
starting,
|
||||||
|
} => {
|
||||||
if let Some(edge) = ending {
|
if let Some(edge) = ending {
|
||||||
if let BoundaryEdge::Line(line) = &mut hatch.paths[path].edges[edge] {
|
if let BoundaryEdge::Line(line) = &mut hatch.paths[path].edges[edge] {
|
||||||
line.end = codec::types::Vector2::new(x, y);
|
line.end = codec::types::Vector2::new(x, y);
|
||||||
@@ -1191,7 +1222,12 @@ fn set_boundary_point(hatch: &mut Hatch, point: BoundaryPoint, x: f64, y: f64) {
|
|||||||
ellipse.center.y = y;
|
ellipse.center.y = y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BoundaryPoint::SplinePoint { path, edge, index, fit } => {
|
BoundaryPoint::SplinePoint {
|
||||||
|
path,
|
||||||
|
edge,
|
||||||
|
index,
|
||||||
|
fit,
|
||||||
|
} => {
|
||||||
if let BoundaryEdge::Spline(spline) = &mut hatch.paths[path].edges[edge] {
|
if let BoundaryEdge::Spline(spline) = &mut hatch.paths[path].edges[edge] {
|
||||||
if fit {
|
if fit {
|
||||||
if let Some(point) = spline.fit_points.get_mut(index) {
|
if let Some(point) = spline.fit_points.get_mut(index) {
|
||||||
@@ -1241,7 +1277,9 @@ fn edit_boundary_vertex(
|
|||||||
A::AddVertex => {
|
A::AddVertex => {
|
||||||
if vertex + 1 == n && !polyline.is_closed {
|
if vertex + 1 == n && !polyline.is_closed {
|
||||||
let last = polyline.vertices[vertex].clone();
|
let last = polyline.vertices[vertex].clone();
|
||||||
polyline.vertices.push(codec::types::Vector3::new(last.x, last.y, 0.0));
|
polyline
|
||||||
|
.vertices
|
||||||
|
.push(codec::types::Vector3::new(last.x, last.y, 0.0));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let a = polyline.vertices[vertex].clone();
|
let a = polyline.vertices[vertex].clone();
|
||||||
@@ -1265,7 +1303,11 @@ fn edit_boundary_vertex(
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BoundaryPoint::LineVertex { path, ending, starting } => {
|
BoundaryPoint::LineVertex {
|
||||||
|
path,
|
||||||
|
ending,
|
||||||
|
starting,
|
||||||
|
} => {
|
||||||
let edges = &mut hatch.paths[path].edges;
|
let edges = &mut hatch.paths[path].edges;
|
||||||
match (action, ending, starting) {
|
match (action, ending, starting) {
|
||||||
(A::AddVertex, _, Some(edge)) => {
|
(A::AddVertex, _, Some(edge)) => {
|
||||||
|
|||||||
@@ -1159,7 +1159,9 @@ pub(crate) fn clipped_mesh(
|
|||||||
if (da > 0.0 && db < 0.0) || (da < 0.0 && db > 0.0) {
|
if (da > 0.0 && db < 0.0) || (da < 0.0 && db > 0.0) {
|
||||||
let t = da / (da - db);
|
let t = da / (da - db);
|
||||||
let (pa, pb) = (vertices[a], vertices[b]);
|
let (pa, pb) = (vertices[a], vertices[b]);
|
||||||
loop_points.push(std::array::from_fn(|axis| pa[axis] + (pb[axis] - pa[axis]) * t));
|
loop_points.push(std::array::from_fn(|axis| {
|
||||||
|
pa[axis] + (pb[axis] - pa[axis]) * t
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if loop_points.len() < 3 {
|
if loop_points.len() < 3 {
|
||||||
|
|||||||
+8
-2
@@ -19,7 +19,10 @@ pub(crate) enum TouchAction {
|
|||||||
Pan(Vector),
|
Pan(Vector),
|
||||||
/// Scale the view by `factor` (above 1 zooms in) about `centre`, in
|
/// Scale the view by `factor` (above 1 zooms in) about `centre`, in
|
||||||
/// window coordinates.
|
/// window coordinates.
|
||||||
Zoom { factor: f32, centre: Point },
|
Zoom {
|
||||||
|
factor: f32,
|
||||||
|
centre: Point,
|
||||||
|
},
|
||||||
Orbit(Vector),
|
Orbit(Vector),
|
||||||
Tap(usize),
|
Tap(usize),
|
||||||
SwipeUp,
|
SwipeUp,
|
||||||
@@ -197,7 +200,10 @@ mod tests {
|
|||||||
fn two_fingers_pan_pinch_and_tap() {
|
fn two_fingers_pan_pinch_and_tap() {
|
||||||
let mut touch = TouchGestures::default();
|
let mut touch = TouchGestures::default();
|
||||||
assert!(touch.event(press(1, 100.0, 100.0)).is_empty());
|
assert!(touch.event(press(1, 100.0, 100.0)).is_empty());
|
||||||
assert_eq!(touch.event(press(2, 200.0, 100.0)), vec![TouchAction::Cancel]);
|
assert_eq!(
|
||||||
|
touch.event(press(2, 200.0, 100.0)),
|
||||||
|
vec![TouchAction::Cancel]
|
||||||
|
);
|
||||||
assert!(touch.active());
|
assert!(touch.active());
|
||||||
// Both fingers slide right: a pan, no zoom.
|
// Both fingers slide right: a pan, no zoom.
|
||||||
touch.event(moved(1, 110.0, 100.0));
|
touch.event(moved(1, 110.0, 100.0));
|
||||||
|
|||||||
+40
-8
@@ -160,7 +160,11 @@ fn mesh_nodes(mesh: &MeshModel, index: usize) -> Option<([Node; 3], [Node; 3])>
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// The last corner of a polygon is stored as its bitwise complement.
|
// The last corner of a polygon is stored as its bitwise complement.
|
||||||
polygon_indices.extend([triangle[0] as i32, triangle[1] as i32, !(triangle[2] as i32)]);
|
polygon_indices.extend([
|
||||||
|
triangle[0] as i32,
|
||||||
|
triangle[1] as i32,
|
||||||
|
!(triangle[2] as i32),
|
||||||
|
]);
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
normals.extend(n.map(|value| value / length));
|
normals.extend(n.map(|value| value / length));
|
||||||
}
|
}
|
||||||
@@ -173,10 +177,18 @@ fn mesh_nodes(mesh: &MeshModel, index: usize) -> Option<([Node; 3], [Node; 3])>
|
|||||||
let name = format!("Mesh{}", index + 1);
|
let name = format!("Mesh{}", index + 1);
|
||||||
let geometry = node(
|
let geometry = node(
|
||||||
"Geometry",
|
"Geometry",
|
||||||
vec![Prop::I64(geometry_id), s(&format!("{name}\x00\x01Geometry")), s("Mesh")],
|
vec![
|
||||||
|
Prop::I64(geometry_id),
|
||||||
|
s(&format!("{name}\x00\x01Geometry")),
|
||||||
|
s("Mesh"),
|
||||||
|
],
|
||||||
vec![
|
vec![
|
||||||
node("Vertices", vec![Prop::F64s(points.concat())], Vec::new()),
|
node("Vertices", vec![Prop::F64s(points.concat())], Vec::new()),
|
||||||
node("PolygonVertexIndex", vec![Prop::I32s(polygon_indices)], Vec::new()),
|
node(
|
||||||
|
"PolygonVertexIndex",
|
||||||
|
vec![Prop::I32s(polygon_indices)],
|
||||||
|
Vec::new(),
|
||||||
|
),
|
||||||
node("GeometryVersion", vec![Prop::I32(124)], Vec::new()),
|
node("GeometryVersion", vec![Prop::I32(124)], Vec::new()),
|
||||||
node(
|
node(
|
||||||
"LayerElementNormal",
|
"LayerElementNormal",
|
||||||
@@ -184,7 +196,11 @@ fn mesh_nodes(mesh: &MeshModel, index: usize) -> Option<([Node; 3], [Node; 3])>
|
|||||||
vec![
|
vec![
|
||||||
node("Version", vec![Prop::I32(101)], Vec::new()),
|
node("Version", vec![Prop::I32(101)], Vec::new()),
|
||||||
node("Name", vec![s("")], Vec::new()),
|
node("Name", vec![s("")], Vec::new()),
|
||||||
node("MappingInformationType", vec![s("ByPolygonVertex")], Vec::new()),
|
node(
|
||||||
|
"MappingInformationType",
|
||||||
|
vec![s("ByPolygonVertex")],
|
||||||
|
Vec::new(),
|
||||||
|
),
|
||||||
node("ReferenceInformationType", vec![s("Direct")], Vec::new()),
|
node("ReferenceInformationType", vec![s("Direct")], Vec::new()),
|
||||||
node("Normals", vec![Prop::F64s(normals)], Vec::new()),
|
node("Normals", vec![Prop::F64s(normals)], Vec::new()),
|
||||||
],
|
],
|
||||||
@@ -196,7 +212,11 @@ fn mesh_nodes(mesh: &MeshModel, index: usize) -> Option<([Node; 3], [Node; 3])>
|
|||||||
node("Version", vec![Prop::I32(101)], Vec::new()),
|
node("Version", vec![Prop::I32(101)], Vec::new()),
|
||||||
node("Name", vec![s("")], Vec::new()),
|
node("Name", vec![s("")], Vec::new()),
|
||||||
node("MappingInformationType", vec![s("AllSame")], Vec::new()),
|
node("MappingInformationType", vec![s("AllSame")], Vec::new()),
|
||||||
node("ReferenceInformationType", vec![s("IndexToDirect")], Vec::new()),
|
node(
|
||||||
|
"ReferenceInformationType",
|
||||||
|
vec![s("IndexToDirect")],
|
||||||
|
Vec::new(),
|
||||||
|
),
|
||||||
node("Materials", vec![Prop::I32s(vec![0])], Vec::new()),
|
node("Materials", vec![Prop::I32s(vec![0])], Vec::new()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -227,7 +247,11 @@ fn mesh_nodes(mesh: &MeshModel, index: usize) -> Option<([Node; 3], [Node; 3])>
|
|||||||
);
|
);
|
||||||
let model = node(
|
let model = node(
|
||||||
"Model",
|
"Model",
|
||||||
vec![Prop::I64(model_id), s(&format!("{name}\x00\x01Model")), s("Mesh")],
|
vec![
|
||||||
|
Prop::I64(model_id),
|
||||||
|
s(&format!("{name}\x00\x01Model")),
|
||||||
|
s("Mesh"),
|
||||||
|
],
|
||||||
vec![
|
vec![
|
||||||
node("Version", vec![Prop::I32(232)], Vec::new()),
|
node("Version", vec![Prop::I32(232)], Vec::new()),
|
||||||
node("Properties70", Vec::new(), Vec::new()),
|
node("Properties70", Vec::new(), Vec::new()),
|
||||||
@@ -328,7 +352,12 @@ pub fn build_fbx(meshes: &[&MeshModel], unit_cm: f64) -> Option<Vec<u8>> {
|
|||||||
axis("FrontAxisSign", -1),
|
axis("FrontAxisSign", -1),
|
||||||
axis("CoordAxis", 0),
|
axis("CoordAxis", 0),
|
||||||
axis("CoordAxisSign", 1),
|
axis("CoordAxisSign", 1),
|
||||||
p("UnitScaleFactor", "double", "Number", vec![Prop::F64(unit_cm)]),
|
p(
|
||||||
|
"UnitScaleFactor",
|
||||||
|
"double",
|
||||||
|
"Number",
|
||||||
|
vec![Prop::F64(unit_cm)],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -437,7 +466,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_triangle_writes_a_well_formed_file() {
|
fn a_triangle_writes_a_well_formed_file() {
|
||||||
let triangle = mesh(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], vec![0, 1, 2]);
|
let triangle = mesh(
|
||||||
|
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
|
||||||
|
vec![0, 1, 2],
|
||||||
|
);
|
||||||
let bytes = build_fbx(&[&triangle], 0.1).expect("fbx");
|
let bytes = build_fbx(&[&triangle], 0.1).expect("fbx");
|
||||||
assert!(bytes.starts_with(HEADER));
|
assert!(bytes.starts_with(HEADER));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -243,7 +243,10 @@ mod windows_impl {
|
|||||||
"ms-settings:defaultapps?registeredAppUser={}",
|
"ms-settings:defaultapps?registeredAppUser={}",
|
||||||
APP_REGISTRY_NAME.replace(' ', "%20")
|
APP_REGISTRY_NAME.replace(' ', "%20")
|
||||||
);
|
);
|
||||||
std::process::Command::new("explorer.exe").arg(uri).spawn().ok()?;
|
std::process::Command::new("explorer.exe")
|
||||||
|
.arg(uri)
|
||||||
|
.spawn()
|
||||||
|
.ok()?;
|
||||||
Some(
|
Some(
|
||||||
"Opened Settings → Default apps — choose Open CAD Studio for .dwg / .dxf there."
|
"Opened Settings → Default apps — choose Open CAD Studio for .dwg / .dxf there."
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
|||||||
+56
-20
@@ -178,7 +178,10 @@ mod browser {
|
|||||||
/// inside the picker) — what save bookkeeping should use.
|
/// inside the picker) — what save bookkeeping should use.
|
||||||
pub fn handle_name(tab_id: u64) -> Option<String> {
|
pub fn handle_name(tab_id: u64) -> Option<String> {
|
||||||
SAVE_HANDLES.with(|handles| {
|
SAVE_HANDLES.with(|handles| {
|
||||||
handles.borrow().get(&tab_id).map(|handle| handle.name.clone())
|
handles
|
||||||
|
.borrow()
|
||||||
|
.get(&tab_id)
|
||||||
|
.map(|handle| handle.name.clone())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,10 +200,8 @@ mod browser {
|
|||||||
/// the bytes against the retained handle (`write_or_download`).
|
/// the bytes against the retained handle (`write_or_download`).
|
||||||
pub fn begin_web_save(tab_id: u64, ext: &str) {
|
pub fn begin_web_save(tab_id: u64, ext: &str) {
|
||||||
PENDING_SAVE_EXTS.with(|exts| {
|
PENDING_SAVE_EXTS.with(|exts| {
|
||||||
exts.borrow_mut().insert(
|
exts.borrow_mut()
|
||||||
tab_id,
|
.insert(tab_id, ext.trim_start_matches('.').to_ascii_lowercase());
|
||||||
ext.trim_start_matches('.').to_ascii_lowercase(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
WEB_SAVE_IN_FLIGHT.with(|in_flight| in_flight.borrow_mut().replace(tab_id));
|
WEB_SAVE_IN_FLIGHT.with(|in_flight| in_flight.borrow_mut().replace(tab_id));
|
||||||
}
|
}
|
||||||
@@ -264,10 +265,11 @@ mod browser {
|
|||||||
let window = web_sys::window().ok_or_else(|| "browser window unavailable".to_string())?;
|
let window = web_sys::window().ok_or_else(|| "browser window unavailable".to_string())?;
|
||||||
// Dynamic call — see the module docs for why the typed web-sys
|
// Dynamic call — see the module docs for why the typed web-sys
|
||||||
// binding (`show_save_file_picker_with_options`) is not used.
|
// binding (`show_save_file_picker_with_options`) is not used.
|
||||||
let picker = js_sys::Reflect::get(window.as_ref(), &JsValue::from_str("showSaveFilePicker"))
|
let picker =
|
||||||
.ok()
|
js_sys::Reflect::get(window.as_ref(), &JsValue::from_str("showSaveFilePicker"))
|
||||||
.and_then(|picker| picker.dyn_into::<js_sys::Function>().ok())
|
.ok()
|
||||||
.ok_or_else(|| "showSaveFilePicker unavailable".to_string())?;
|
.and_then(|picker| picker.dyn_into::<js_sys::Function>().ok())
|
||||||
|
.ok_or_else(|| "showSaveFilePicker unavailable".to_string())?;
|
||||||
let options = picker_options(suggested_name, ext);
|
let options = picker_options(suggested_name, ext);
|
||||||
let promise = picker
|
let promise = picker
|
||||||
.call1(window.as_ref(), &options)
|
.call1(window.as_ref(), &options)
|
||||||
@@ -315,7 +317,10 @@ mod browser {
|
|||||||
/// the same way for the rest of the session.
|
/// the same way for the rest of the session.
|
||||||
pub async fn write_handle(tab_id: u64, bytes: &[u8]) -> Result<(), String> {
|
pub async fn write_handle(tab_id: u64, bytes: &[u8]) -> Result<(), String> {
|
||||||
let handle = SAVE_HANDLES.with(|handles| {
|
let handle = SAVE_HANDLES.with(|handles| {
|
||||||
handles.borrow().get(&tab_id).map(|save| save.handle.clone())
|
handles
|
||||||
|
.borrow()
|
||||||
|
.get(&tab_id)
|
||||||
|
.map(|save| save.handle.clone())
|
||||||
});
|
});
|
||||||
let Some(handle) = handle else {
|
let Some(handle) = handle else {
|
||||||
return Err("no retained save destination for this drawing".to_string());
|
return Err("no retained save destination for this drawing".to_string());
|
||||||
@@ -362,7 +367,10 @@ mod browser {
|
|||||||
};
|
};
|
||||||
let accept = js_sys::Object::new();
|
let accept = js_sys::Object::new();
|
||||||
let extensions = js_sys::Array::new();
|
let extensions = js_sys::Array::new();
|
||||||
extensions.push(&JsValue::from_str(&format!(".{}", ext.trim_start_matches('.'))));
|
extensions.push(&JsValue::from_str(&format!(
|
||||||
|
".{}",
|
||||||
|
ext.trim_start_matches('.')
|
||||||
|
)));
|
||||||
set(&accept, super::accept_mime(ext), &extensions);
|
set(&accept, super::accept_mime(ext), &extensions);
|
||||||
let type_entry = js_sys::Object::new();
|
let type_entry = js_sys::Object::new();
|
||||||
set(
|
set(
|
||||||
@@ -378,7 +386,11 @@ mod browser {
|
|||||||
let types = js_sys::Array::new();
|
let types = js_sys::Array::new();
|
||||||
types.push(&type_entry);
|
types.push(&type_entry);
|
||||||
let options = js_sys::Object::new();
|
let options = js_sys::Object::new();
|
||||||
set(&options, "suggestedName", &JsValue::from_str(suggested_name));
|
set(
|
||||||
|
&options,
|
||||||
|
"suggestedName",
|
||||||
|
&JsValue::from_str(suggested_name),
|
||||||
|
);
|
||||||
set(&options, "types", &types);
|
set(&options, "types", &types);
|
||||||
options
|
options
|
||||||
}
|
}
|
||||||
@@ -398,7 +410,10 @@ mod tests {
|
|||||||
/// whatever handle state the tab happens to hold.
|
/// whatever handle state the tab happens to hold.
|
||||||
#[test]
|
#[test]
|
||||||
fn unsupported_browsers_always_download() {
|
fn unsupported_browsers_always_download() {
|
||||||
assert_eq!(save_route(false, true, Some("dwg"), "dwg"), SaveRoute::Download);
|
assert_eq!(
|
||||||
|
save_route(false, true, Some("dwg"), "dwg"),
|
||||||
|
SaveRoute::Download
|
||||||
|
);
|
||||||
assert_eq!(save_route(false, false, None, "dxf"), SaveRoute::Download);
|
assert_eq!(save_route(false, false, None, "dxf"), SaveRoute::Download);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,30 +421,51 @@ mod tests {
|
|||||||
/// in place — no picker, no download.
|
/// in place — no picker, no download.
|
||||||
#[test]
|
#[test]
|
||||||
fn plain_save_with_matching_handle_writes_in_place() {
|
fn plain_save_with_matching_handle_writes_in_place() {
|
||||||
assert_eq!(save_route(true, true, Some("dwg"), "dwg"), SaveRoute::WriteInPlace);
|
assert_eq!(
|
||||||
assert_eq!(save_route(true, true, Some("dxf"), "dxf"), SaveRoute::WriteInPlace);
|
save_route(true, true, Some("dwg"), "dwg"),
|
||||||
|
SaveRoute::WriteInPlace
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
save_route(true, true, Some("dxf"), "dxf"),
|
||||||
|
SaveRoute::WriteInPlace
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extension comparison is case-insensitive and dot-tolerant — the
|
/// Extension comparison is case-insensitive and dot-tolerant — the
|
||||||
/// registry stores the normalized form, the format table's is bare.
|
/// registry stores the normalized form, the format table's is bare.
|
||||||
#[test]
|
#[test]
|
||||||
fn handle_ext_match_ignores_case_and_dot() {
|
fn handle_ext_match_ignores_case_and_dot() {
|
||||||
assert_eq!(save_route(true, true, Some(".DWG"), "dwg"), SaveRoute::WriteInPlace);
|
assert_eq!(
|
||||||
assert_eq!(save_route(true, true, Some("dwg"), ".DWG"), SaveRoute::WriteInPlace);
|
save_route(true, true, Some(".DWG"), "dwg"),
|
||||||
|
SaveRoute::WriteInPlace
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
save_route(true, true, Some("dwg"), ".DWG"),
|
||||||
|
SaveRoute::WriteInPlace
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A plain Save targeting the other format must re-pick: writing DXF
|
/// A plain Save targeting the other format must re-pick: writing DXF
|
||||||
/// bytes through a .dwg handle would corrupt the file's identity.
|
/// bytes through a .dwg handle would corrupt the file's identity.
|
||||||
#[test]
|
#[test]
|
||||||
fn mismatched_format_re_picks() {
|
fn mismatched_format_re_picks() {
|
||||||
assert_eq!(save_route(true, true, Some("dwg"), "dxf"), SaveRoute::PickHandle);
|
assert_eq!(
|
||||||
assert_eq!(save_route(true, true, Some("dxf"), "dwg"), SaveRoute::PickHandle);
|
save_route(true, true, Some("dwg"), "dxf"),
|
||||||
|
SaveRoute::PickHandle
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
save_route(true, true, Some("dxf"), "dwg"),
|
||||||
|
SaveRoute::PickHandle
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save As never reuses the handle — the user asked for a destination.
|
/// Save As never reuses the handle — the user asked for a destination.
|
||||||
#[test]
|
#[test]
|
||||||
fn save_as_with_existing_handle_re_picks() {
|
fn save_as_with_existing_handle_re_picks() {
|
||||||
assert_eq!(save_route(true, false, Some("dwg"), "dwg"), SaveRoute::PickHandle);
|
assert_eq!(
|
||||||
|
save_route(true, false, Some("dwg"), "dwg"),
|
||||||
|
SaveRoute::PickHandle
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// No handle yet (first save, or the previous write dropped it): pick.
|
/// No handle yet (first save, or the previous write dropped it): pick.
|
||||||
|
|||||||
+4
-1
@@ -236,7 +236,10 @@ const LIN_SOURCE: &str = include_str!("../../assets/linetypes/OpenCADStudio.lin"
|
|||||||
fn lin_source() -> &'static str {
|
fn lin_source() -> &'static str {
|
||||||
static SOURCE: OnceLock<String> = OnceLock::new();
|
static SOURCE: OnceLock<String> = OnceLock::new();
|
||||||
SOURCE.get_or_init(|| {
|
SOURCE.get_or_init(|| {
|
||||||
format!("{LIN_SOURCE}\n{}", crate::io::patterns::user_definitions("linetypes", "lin"))
|
format!(
|
||||||
|
"{LIN_SOURCE}\n{}",
|
||||||
|
crate::io::patterns::user_definitions("linetypes", "lin")
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-23
@@ -8,9 +8,10 @@ pub mod edit_lock;
|
|||||||
// Same split as web_session/web_autosave: the save-routing decision (and
|
// Same split as web_session/web_autosave: the save-routing decision (and
|
||||||
// its tests) is pure and compiles everywhere; the showSaveFilePicker half
|
// its tests) is pure and compiles everywhere; the showSaveFilePicker half
|
||||||
// inside is wasm-only.
|
// inside is wasm-only.
|
||||||
pub(crate) mod fs_access;
|
pub mod fbx;
|
||||||
pub mod file_association;
|
pub mod file_association;
|
||||||
pub mod font_repo;
|
pub mod font_repo;
|
||||||
|
pub(crate) mod fs_access;
|
||||||
pub mod line_read;
|
pub mod line_read;
|
||||||
pub mod linetypes;
|
pub mod linetypes;
|
||||||
pub mod obj;
|
pub mod obj;
|
||||||
@@ -25,7 +26,6 @@ pub mod print_to_printer;
|
|||||||
pub mod recovery;
|
pub mod recovery;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod single_instance;
|
pub mod single_instance;
|
||||||
pub mod fbx;
|
|
||||||
pub mod step;
|
pub mod step;
|
||||||
pub mod stl;
|
pub mod stl;
|
||||||
pub mod svg_export;
|
pub mod svg_export;
|
||||||
@@ -38,9 +38,9 @@ pub(crate) mod web_recent;
|
|||||||
// the native host; the OPFS half inside is wasm-only.
|
// the native host; the OPFS half inside is wasm-only.
|
||||||
pub(crate) mod web_autosave;
|
pub(crate) mod web_autosave;
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
mod web_worker;
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
pub(crate) mod web_drop;
|
pub(crate) mod web_drop;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
mod web_worker;
|
||||||
// Same split as web_autosave: the ?file= deep-link parser (and its tests) are
|
// Same split as web_autosave: the ?file= deep-link parser (and its tests) are
|
||||||
// pure and compile everywhere; the fetch/open half is wasm-only.
|
// pure and compile everywhere; the fetch/open half is wasm-only.
|
||||||
pub(crate) mod web_link;
|
pub(crate) mod web_link;
|
||||||
@@ -57,9 +57,9 @@ use codec::entities::EntityType;
|
|||||||
// parse worker / the xref bytes registry instead.
|
// parse worker / the xref bytes registry instead.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use codec::io::dwg::DwgReader;
|
use codec::io::dwg::DwgReader;
|
||||||
|
use codec::{CadDocument, DwgWriter, DxfWriter};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use codec::{DwgReadOptions, DxfReader, DxfReaderConfiguration};
|
use codec::{DwgReadOptions, DxfReader, DxfReaderConfiguration};
|
||||||
use codec::{CadDocument, DwgWriter, DxfWriter};
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicU16, AtomicU32, AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicU16, AtomicU32, AtomicU8, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -293,7 +293,10 @@ pub async fn pick_linetype_file_web() -> Option<(String, String)> {
|
|||||||
pub async fn pick_datalink_csv_web() -> Option<(String, Vec<u8>)> {
|
pub async fn pick_datalink_csv_web() -> Option<(String, Vec<u8>)> {
|
||||||
let Some(handle) = crate::sys::file_dialog()
|
let Some(handle) = crate::sys::file_dialog()
|
||||||
.set_title(crate::t!("Link a CSV data source").as_ref())
|
.set_title(crate::t!("Link a CSV data source").as_ref())
|
||||||
.add_filter(crate::t!("CSV Files").as_ref(), &["csv", "txt", "CSV", "TXT"])
|
.add_filter(
|
||||||
|
crate::t!("CSV Files").as_ref(),
|
||||||
|
&["csv", "txt", "CSV", "TXT"],
|
||||||
|
)
|
||||||
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||||
.pick_file()
|
.pick_file()
|
||||||
.await
|
.await
|
||||||
@@ -2003,8 +2006,7 @@ pub async fn pick_plot_style() -> Result<Option<plot_style::PlotStyleTable>, Str
|
|||||||
{
|
{
|
||||||
let path = crate::sys::handle_path(&handle);
|
let path = crate::sys::handle_path(&handle);
|
||||||
let table = plot_style::PlotStyleTable::load(&path)?;
|
let table = plot_style::PlotStyleTable::load(&path)?;
|
||||||
if let (Ok(dir), Some(file_name)) =
|
if let (Ok(dir), Some(file_name)) = (plot_style::ensure_plot_styles_dir(), path.file_name())
|
||||||
(plot_style::ensure_plot_styles_dir(), path.file_name())
|
|
||||||
{
|
{
|
||||||
let destination = dir.join(file_name);
|
let destination = dir.join(file_name);
|
||||||
let same_place = path.parent().is_some_and(|parent| {
|
let same_place = path.parent().is_some_and(|parent| {
|
||||||
@@ -3534,10 +3536,8 @@ mod prepare_save_tests {
|
|||||||
fn prepare_bakes_missing_dimension_blocks() {
|
fn prepare_bakes_missing_dimension_blocks() {
|
||||||
let mut doc = CadDocument::new();
|
let mut doc = CadDocument::new();
|
||||||
crate::io::linetypes::populate_document(&mut doc);
|
crate::io::linetypes::populate_document(&mut doc);
|
||||||
let mut dim = DimensionLinear::new(
|
let mut dim =
|
||||||
Vector3::new(0.0, 0.0, 0.0),
|
DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
|
||||||
Vector3::new(10.0, 0.0, 0.0),
|
|
||||||
);
|
|
||||||
dim.definition_point = Vector3::new(5.0, 7.0, 0.0);
|
dim.definition_point = Vector3::new(5.0, 7.0, 0.0);
|
||||||
dim.base.definition_point = Vector3::ZERO;
|
dim.base.definition_point = Vector3::ZERO;
|
||||||
dim.base.common.handle = doc.allocate_handle();
|
dim.base.common.handle = doc.allocate_handle();
|
||||||
@@ -3546,8 +3546,7 @@ mod prepare_save_tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let prepared = prepare_document_for_save(doc.clone(), doc.version);
|
let prepared = prepare_document_for_save(doc.clone(), doc.version);
|
||||||
let EntityType::Dimension(Dimension::Linear(linear)) =
|
let EntityType::Dimension(Dimension::Linear(linear)) = prepared.get_entity(handle).unwrap()
|
||||||
prepared.get_entity(handle).unwrap()
|
|
||||||
else {
|
else {
|
||||||
panic!("dimension missing from prepared clone");
|
panic!("dimension missing from prepared clone");
|
||||||
};
|
};
|
||||||
@@ -3556,13 +3555,14 @@ mod prepare_save_tests {
|
|||||||
"anonymous dimension block not baked: {}",
|
"anonymous dimension block not baked: {}",
|
||||||
linear.base.block_name
|
linear.base.block_name
|
||||||
);
|
);
|
||||||
assert!(prepared.block_records.get(&linear.base.block_name).is_some());
|
assert!(prepared
|
||||||
|
.block_records
|
||||||
|
.get(&linear.base.block_name)
|
||||||
|
.is_some());
|
||||||
assert_eq!(linear.base.definition_point, linear.definition_point);
|
assert_eq!(linear.base.definition_point, linear.definition_point);
|
||||||
|
|
||||||
// The live document is untouched: no block name, stale group-10.
|
// The live document is untouched: no block name, stale group-10.
|
||||||
let EntityType::Dimension(Dimension::Linear(live)) =
|
let EntityType::Dimension(Dimension::Linear(live)) = doc.get_entity(handle).unwrap() else {
|
||||||
doc.get_entity(handle).unwrap()
|
|
||||||
else {
|
|
||||||
panic!("dimension missing from live document");
|
panic!("dimension missing from live document");
|
||||||
};
|
};
|
||||||
assert!(live.base.block_name.is_empty());
|
assert!(live.base.block_name.is_empty());
|
||||||
@@ -3576,10 +3576,8 @@ mod prepare_save_tests {
|
|||||||
fn save_to_bytes_equals_prepare_then_shared_serialize() {
|
fn save_to_bytes_equals_prepare_then_shared_serialize() {
|
||||||
let mut doc = CadDocument::new();
|
let mut doc = CadDocument::new();
|
||||||
crate::io::linetypes::populate_document(&mut doc);
|
crate::io::linetypes::populate_document(&mut doc);
|
||||||
let mut dim = DimensionLinear::new(
|
let mut dim =
|
||||||
Vector3::new(0.0, 0.0, 0.0),
|
DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
|
||||||
Vector3::new(10.0, 0.0, 0.0),
|
|
||||||
);
|
|
||||||
dim.base.common.handle = doc.allocate_handle();
|
dim.base.common.handle = doc.allocate_handle();
|
||||||
doc.add_entity(EntityType::Dimension(Dimension::Linear(dim)))
|
doc.add_entity(EntityType::Dimension(Dimension::Linear(dim)))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -3587,7 +3585,10 @@ mod prepare_save_tests {
|
|||||||
let inline = save_to_bytes(&doc, ext, codec::DxfVersion::AC1032).expect("inline");
|
let inline = save_to_bytes(&doc, ext, codec::DxfVersion::AC1032).expect("inline");
|
||||||
let prepared = prepare_document_for_save(doc.clone(), codec::DxfVersion::AC1032);
|
let prepared = prepare_document_for_save(doc.clone(), codec::DxfVersion::AC1032);
|
||||||
let shared = ocs_save::serialize_document(&prepared, ext).expect("shared");
|
let shared = ocs_save::serialize_document(&prepared, ext).expect("shared");
|
||||||
assert_eq!(inline, shared, "inline and shared serialize diverge for {ext}");
|
assert_eq!(
|
||||||
|
inline, shared,
|
||||||
|
"inline and shared serialize diverge for {ext}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+10
-2
@@ -42,7 +42,12 @@ static CATALOG: OnceLock<Vec<PatternDef>> = OnceLock::new();
|
|||||||
/// All parsed patterns, in file order: the bundled catalog, then the user's
|
/// All parsed patterns, in file order: the bundled catalog, then the user's
|
||||||
/// own `.pat` files.
|
/// own `.pat` files.
|
||||||
pub fn catalog() -> &'static [PatternDef] {
|
pub fn catalog() -> &'static [PatternDef] {
|
||||||
CATALOG.get_or_init(|| parse(&format!("{PAT_SRC}\n{}", user_definitions("patterns", "pat"))))
|
CATALOG.get_or_init(|| {
|
||||||
|
parse(&format!(
|
||||||
|
"{PAT_SRC}\n{}",
|
||||||
|
user_definitions("patterns", "pat")
|
||||||
|
))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The text of every `*.<ext>` file in `<config>/<folder>`, where users drop
|
/// The text of every `*.<ext>` file in `<config>/<folder>`, where users drop
|
||||||
@@ -59,7 +64,10 @@ pub(crate) fn user_definitions(folder: &str, ext: &str) -> String {
|
|||||||
let mut paths: Vec<_> = entries
|
let mut paths: Vec<_> = entries
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.map(|entry| entry.path())
|
.map(|entry| entry.path())
|
||||||
.filter(|path| path.extension().is_some_and(|e| e.eq_ignore_ascii_case(ext)))
|
.filter(|path| {
|
||||||
|
path.extension()
|
||||||
|
.is_some_and(|e| e.eq_ignore_ascii_case(ext))
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
paths.sort();
|
paths.sort();
|
||||||
paths
|
paths
|
||||||
|
|||||||
+18
-4
@@ -185,7 +185,8 @@ fn emit_image(
|
|||||||
let mut height = image.height;
|
let mut height = image.height;
|
||||||
let mut pixels: std::borrow::Cow<[u8]> = std::borrow::Cow::Borrowed(image.pixels.as_slice());
|
let mut pixels: std::borrow::Cow<[u8]> = std::borrow::Cow::Borrowed(image.pixels.as_slice());
|
||||||
if let Some(target) = options.quality.target_dpi() {
|
if let Some(target) = options.quality.target_dpi() {
|
||||||
let dist = |a: [f32; 2], b: [f32; 2]| ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
|
let dist =
|
||||||
|
|a: [f32; 2], b: [f32; 2]| ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
|
||||||
let width_edge = dist(corners[0], corners[1]);
|
let width_edge = dist(corners[0], corners[1]);
|
||||||
let height_edge = dist(corners[0], corners[3]);
|
let height_edge = dist(corners[0], corners[3]);
|
||||||
// A degenerate (zero-area) placement keeps the native pixels.
|
// A degenerate (zero-area) placement keeps the native pixels.
|
||||||
@@ -208,8 +209,12 @@ fn emit_image(
|
|||||||
if let Some(buffer) =
|
if let Some(buffer) =
|
||||||
image::RgbaImage::from_raw(width, height, image.pixels.as_ref().clone())
|
image::RgbaImage::from_raw(width, height, image.pixels.as_ref().clone())
|
||||||
{
|
{
|
||||||
let resized =
|
let resized = image::imageops::resize(
|
||||||
image::imageops::resize(&buffer, w, h, image::imageops::FilterType::Triangle);
|
&buffer,
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
image::imageops::FilterType::Triangle,
|
||||||
|
);
|
||||||
width = w;
|
width = w;
|
||||||
height = h;
|
height = h;
|
||||||
pixels = std::borrow::Cow::Owned(resized.into_raw());
|
pixels = std::borrow::Cow::Owned(resized.into_raw());
|
||||||
@@ -751,7 +756,16 @@ fn append_pdf_page(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
DrawItem::Image(image) => {
|
DrawItem::Image(image) => {
|
||||||
emit_image(doc, image_resources, &mut ops, image, ox, oy, scale, options)?;
|
emit_image(
|
||||||
|
doc,
|
||||||
|
image_resources,
|
||||||
|
&mut ops,
|
||||||
|
image,
|
||||||
|
ox,
|
||||||
|
oy,
|
||||||
|
scale,
|
||||||
|
options,
|
||||||
|
)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
DrawItem::Wire(wire) => wire,
|
DrawItem::Wire(wire) => wire,
|
||||||
|
|||||||
+1
-1
@@ -7,9 +7,9 @@
|
|||||||
// the historical `Pdf*` names as aliases so existing call sites keep working.
|
// the historical `Pdf*` names as aliases so existing call sites keep working.
|
||||||
|
|
||||||
use crate::io::plot_style::PlotStyleTable;
|
use crate::io::plot_style::PlotStyleTable;
|
||||||
use crate::scene::WireModel;
|
|
||||||
use crate::scene::model::hatch_model::HatchModel;
|
use crate::scene::model::hatch_model::HatchModel;
|
||||||
use crate::scene::model::image_model::ImageModel;
|
use crate::scene::model::image_model::ImageModel;
|
||||||
|
use crate::scene::WireModel;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||||
|
|||||||
+145
-34
@@ -63,8 +63,7 @@ pub fn export_svg_pages(
|
|||||||
.unwrap_or_else(|| "drawing".to_string());
|
.unwrap_or_else(|| "drawing".to_string());
|
||||||
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
||||||
for (index, page) in pages.iter().enumerate() {
|
for (index, page) in pages.iter().enumerate() {
|
||||||
let svg = build_svg(page, plot_style)
|
let svg = build_svg(page, plot_style).map_err(|e| format!("Page {}: {e}", index + 1))?;
|
||||||
.map_err(|e| format!("Page {}: {e}", index + 1))?;
|
|
||||||
let numbered = parent.join(format!("{stem}-{}.svg", index + 1));
|
let numbered = parent.join(format!("{stem}-{}.svg", index + 1));
|
||||||
write_svg_atomically(&numbered, svg.as_bytes())
|
write_svg_atomically(&numbered, svg.as_bytes())
|
||||||
.map_err(|e| format!("Page {}: {e}", index + 1))?;
|
.map_err(|e| format!("Page {}: {e}", index + 1))?;
|
||||||
@@ -128,7 +127,11 @@ pub fn build_svg(
|
|||||||
));
|
));
|
||||||
out.push_str(">\n<title>Open CAD Studio plot</title>\n");
|
out.push_str(">\n<title>Open CAD Studio plot</title>\n");
|
||||||
out.push_str("<rect x=\"0\" y=\"0\"");
|
out.push_str("<rect x=\"0\" y=\"0\"");
|
||||||
out.push_str(&format!(" width=\"{}\" height=\"{}\" fill=\"#ffffff\"/>\n", fmt(paper_w), fmt(paper_h)));
|
out.push_str(&format!(
|
||||||
|
" width=\"{}\" height=\"{}\" fill=\"#ffffff\"/>\n",
|
||||||
|
fmt(paper_w),
|
||||||
|
fmt(paper_h)
|
||||||
|
));
|
||||||
|
|
||||||
let mut clip_id: Option<String> = None;
|
let mut clip_id: Option<String> = None;
|
||||||
if let Some((cx, cy, cw, ch)) = page.clip {
|
if let Some((cx, cy, cw, ch)) = page.clip {
|
||||||
@@ -140,14 +143,25 @@ pub fn build_svg(
|
|||||||
map.apply((cx + cw) as f64, (cy + ch) as f64),
|
map.apply((cx + cw) as f64, (cy + ch) as f64),
|
||||||
map.apply(cx as f64, (cy + ch) as f64),
|
map.apply(cx as f64, (cy + ch) as f64),
|
||||||
];
|
];
|
||||||
let (mut x0, mut y0, mut x1, mut y1) = (f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
|
let (mut x0, mut y0, mut x1, mut y1) = (
|
||||||
|
f64::INFINITY,
|
||||||
|
f64::INFINITY,
|
||||||
|
f64::NEG_INFINITY,
|
||||||
|
f64::NEG_INFINITY,
|
||||||
|
);
|
||||||
for (x, y) in corners {
|
for (x, y) in corners {
|
||||||
x0 = x0.min(x);
|
x0 = x0.min(x);
|
||||||
y0 = y0.min(y);
|
y0 = y0.min(y);
|
||||||
x1 = x1.max(x);
|
x1 = x1.max(x);
|
||||||
y1 = y1.max(y);
|
y1 = y1.max(y);
|
||||||
}
|
}
|
||||||
if x1 > x0 && y1 > y0 && x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite() {
|
if x1 > x0
|
||||||
|
&& y1 > y0
|
||||||
|
&& x0.is_finite()
|
||||||
|
&& y0.is_finite()
|
||||||
|
&& x1.is_finite()
|
||||||
|
&& y1.is_finite()
|
||||||
|
{
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"<clipPath id=\"plot-clip\"><rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\"/></clipPath>\n",
|
"<clipPath id=\"plot-clip\"><rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\"/></clipPath>\n",
|
||||||
fmt(x0), fmt(y0), fmt(x1 - x0), fmt(y1 - y0)
|
fmt(x0), fmt(y0), fmt(x1 - x0), fmt(y1 - y0)
|
||||||
@@ -238,7 +252,8 @@ fn emit_page_content(
|
|||||||
(first_wires, first_hatches, first_wipeouts, first_images),
|
(first_wires, first_hatches, first_wipeouts, first_images),
|
||||||
(second_wires, second_hatches, second_wipeouts, second_images),
|
(second_wires, second_hatches, second_wipeouts, second_images),
|
||||||
] {
|
] {
|
||||||
let mut items = Vec::with_capacity(wires.len() * 2 + hatches.len() + wipeouts.len() + images.len());
|
let mut items =
|
||||||
|
Vec::with_capacity(wires.len() * 2 + hatches.len() + wipeouts.len() + images.len());
|
||||||
let mut sequence = 0usize;
|
let mut sequence = 0usize;
|
||||||
for wire in wires {
|
for wire in wires {
|
||||||
if !wire.fill_tris.is_empty() {
|
if !wire.fill_tris.is_empty() {
|
||||||
@@ -257,7 +272,12 @@ fn emit_page_content(
|
|||||||
sequence += 1;
|
sequence += 1;
|
||||||
}
|
}
|
||||||
for image in images {
|
for image in images {
|
||||||
items.push((image.image.draw_depth, 1u8, sequence, DrawItem::Image(image)));
|
items.push((
|
||||||
|
image.image.draw_depth,
|
||||||
|
1u8,
|
||||||
|
sequence,
|
||||||
|
DrawItem::Image(image),
|
||||||
|
));
|
||||||
sequence += 1;
|
sequence += 1;
|
||||||
}
|
}
|
||||||
items.sort_by(|a, b| {
|
items.sort_by(|a, b| {
|
||||||
@@ -267,10 +287,16 @@ fn emit_page_content(
|
|||||||
});
|
});
|
||||||
for (_, _, _, item) in items {
|
for (_, _, _, item) in items {
|
||||||
match item {
|
match item {
|
||||||
DrawItem::WireFill(wire) => emit_wire_fills(out, &wire.wire, ox, oy, plot_style, options, map),
|
DrawItem::WireFill(wire) => {
|
||||||
|
emit_wire_fills(out, &wire.wire, ox, oy, plot_style, options, map)
|
||||||
|
}
|
||||||
DrawItem::Hatch(hatch) => emit_hatch(out, hatch, ox, oy, plot_style, options, map),
|
DrawItem::Hatch(hatch) => emit_hatch(out, hatch, ox, oy, plot_style, options, map),
|
||||||
DrawItem::Text(wire) => emit_searchable_text(out, &wire.wire, ox, oy, plot_style, options, map),
|
DrawItem::Text(wire) => {
|
||||||
DrawItem::Wire(wire) => emit_wire(out, &wire.wire, ox, oy, plot_style, options, map),
|
emit_searchable_text(out, &wire.wire, ox, oy, plot_style, options, map)
|
||||||
|
}
|
||||||
|
DrawItem::Wire(wire) => {
|
||||||
|
emit_wire(out, &wire.wire, ox, oy, plot_style, options, map)
|
||||||
|
}
|
||||||
DrawItem::Image(image) => emit_image(out, image, ox, oy, options, map)?,
|
DrawItem::Image(image) => emit_image(out, image, ox, oy, options, map)?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,7 +320,10 @@ fn emit_wire(
|
|||||||
if a < 0.01 {
|
if a < 0.01 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if matches!(wire.name.as_str(), "__paper_boundary__" | "paper_printable_area") {
|
if matches!(
|
||||||
|
wire.name.as_str(),
|
||||||
|
"__paper_boundary__" | "paper_printable_area"
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut lw_override_mm: Option<f64> = None;
|
let mut lw_override_mm: Option<f64> = None;
|
||||||
@@ -368,7 +397,12 @@ fn emit_wire(
|
|||||||
pen_down = false;
|
pen_down = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
d.push_str(&format!("{} {} {} ", if pen_down { 'L' } else { 'M' }, fmt(sx), fmt(sy)));
|
d.push_str(&format!(
|
||||||
|
"{} {} {} ",
|
||||||
|
if pen_down { 'L' } else { 'M' },
|
||||||
|
fmt(sx),
|
||||||
|
fmt(sy)
|
||||||
|
));
|
||||||
pen_down = true;
|
pen_down = true;
|
||||||
any = true;
|
any = true;
|
||||||
}
|
}
|
||||||
@@ -400,7 +434,11 @@ fn emit_wire_fills(
|
|||||||
if a < 0.01 {
|
if a < 0.01 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if wire.bg_adapt.as_deref().is_some_and(|adapt| adapt.canvas_color) {
|
if wire
|
||||||
|
.bg_adapt
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|adapt| adapt.canvas_color)
|
||||||
|
{
|
||||||
[r, g, b] = [1.0, 1.0, 1.0];
|
[r, g, b] = [1.0, 1.0, 1.0];
|
||||||
} else {
|
} else {
|
||||||
let mut screening = 1.0;
|
let mut screening = 1.0;
|
||||||
@@ -424,7 +462,11 @@ fn emit_wire_fills(
|
|||||||
let mut pts = Vec::with_capacity(3);
|
let mut pts = Vec::with_capacity(3);
|
||||||
let mut ok = true;
|
let mut ok = true;
|
||||||
for (pi, &[x, y, _]) in tri.iter().enumerate() {
|
for (pi, &[x, y, _]) in tri.iter().enumerate() {
|
||||||
let low = wire.fill_tris_low.get(ti * 3 + pi).copied().unwrap_or([0.0; 3]);
|
let low = wire
|
||||||
|
.fill_tris_low
|
||||||
|
.get(ti * 3 + pi)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or([0.0; 3]);
|
||||||
let (sx, sy) = map.apply(x as f64 + low[0] as f64 + ox, y as f64 + low[1] as f64 + oy);
|
let (sx, sy) = map.apply(x as f64 + low[0] as f64 + ox, y as f64 + low[1] as f64 + oy);
|
||||||
if !sx.is_finite() || !sy.is_finite() {
|
if !sx.is_finite() || !sy.is_finite() {
|
||||||
ok = false;
|
ok = false;
|
||||||
@@ -437,9 +479,12 @@ fn emit_wire_fills(
|
|||||||
}
|
}
|
||||||
d.push_str(&format!(
|
d.push_str(&format!(
|
||||||
"M {} {} L {} {} L {} {} Z ",
|
"M {} {} L {} {} L {} {} Z ",
|
||||||
fmt(pts[0].0), fmt(pts[0].1),
|
fmt(pts[0].0),
|
||||||
fmt(pts[1].0), fmt(pts[1].1),
|
fmt(pts[0].1),
|
||||||
fmt(pts[2].0), fmt(pts[2].1),
|
fmt(pts[1].0),
|
||||||
|
fmt(pts[1].1),
|
||||||
|
fmt(pts[2].0),
|
||||||
|
fmt(pts[2].1),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if d.is_empty() {
|
if d.is_empty() {
|
||||||
@@ -490,7 +535,10 @@ fn emit_hatch(
|
|||||||
}
|
}
|
||||||
if is_wipeout {
|
if is_wipeout {
|
||||||
[r, g, b] = [1.0, 1.0, 1.0];
|
[r, g, b] = [1.0, 1.0, 1.0];
|
||||||
} else if !color_overridden && hatch.aci != 0 && !(hatch.aci == 7 && matches!(hatch.pattern, HatchPattern::Solid)) {
|
} else if !color_overridden
|
||||||
|
&& hatch.aci != 0
|
||||||
|
&& !(hatch.aci == 7 && matches!(hatch.pattern, HatchPattern::Solid))
|
||||||
|
{
|
||||||
let is_light = r > 0.80
|
let is_light = r > 0.80
|
||||||
&& g > 0.80
|
&& g > 0.80
|
||||||
&& b > 0.80
|
&& b > 0.80
|
||||||
@@ -514,7 +562,11 @@ fn emit_hatch(
|
|||||||
0.1 * PT_TO_MM
|
0.1 * PT_TO_MM
|
||||||
}
|
}
|
||||||
.max(0.1 * PT_TO_MM);
|
.max(0.1 * PT_TO_MM);
|
||||||
let width_mm = if options.scale_lineweights { physical * s } else { physical };
|
let width_mm = if options.scale_lineweights {
|
||||||
|
physical * s
|
||||||
|
} else {
|
||||||
|
physical
|
||||||
|
};
|
||||||
let segments = hatch.pattern_segments_for_plot();
|
let segments = hatch.pattern_segments_for_plot();
|
||||||
if segments.is_empty() {
|
if segments.is_empty() {
|
||||||
return;
|
return;
|
||||||
@@ -531,7 +583,13 @@ fn emit_hatch(
|
|||||||
let diam = fmt(width_mm.max(0.04));
|
let diam = fmt(width_mm.max(0.04));
|
||||||
d.push_str(&format!("M {x} {y} m -{rad} 0 a {rad} {rad} 0 1 0 {diam} 0 a {rad} {rad} 0 1 0 -{diam} 0 ", x = fmt(ax), y = fmt(ay)));
|
d.push_str(&format!("M {x} {y} m -{rad} 0 a {rad} {rad} 0 1 0 {diam} 0 a {rad} {rad} 0 1 0 -{diam} 0 ", x = fmt(ax), y = fmt(ay)));
|
||||||
} else {
|
} else {
|
||||||
d.push_str(&format!("M {} {} L {} {} ", fmt(ax), fmt(ay), fmt(bx), fmt(by)));
|
d.push_str(&format!(
|
||||||
|
"M {} {} L {} {} ",
|
||||||
|
fmt(ax),
|
||||||
|
fmt(ay),
|
||||||
|
fmt(bx),
|
||||||
|
fmt(by)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if d.is_empty() {
|
if d.is_empty() {
|
||||||
@@ -554,7 +612,11 @@ fn emit_hatch(
|
|||||||
screening,
|
screening,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
[r, g, b] = [(r + second[0]) * 0.5, (g + second[1]) * 0.5, (b + second[2]) * 0.5];
|
[r, g, b] = [
|
||||||
|
(r + second[0]) * 0.5,
|
||||||
|
(g + second[1]) * 0.5,
|
||||||
|
(b + second[2]) * 0.5,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut d = String::new();
|
let mut d = String::new();
|
||||||
@@ -572,7 +634,12 @@ fn emit_hatch(
|
|||||||
if !sx.is_finite() || !sy.is_finite() {
|
if !sx.is_finite() || !sy.is_finite() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
d.push_str(&format!("{} {} {} ", if current_empty { 'M' } else { 'L' }, fmt(sx), fmt(sy)));
|
d.push_str(&format!(
|
||||||
|
"{} {} {} ",
|
||||||
|
if current_empty { 'M' } else { 'L' },
|
||||||
|
fmt(sx),
|
||||||
|
fmt(sy)
|
||||||
|
));
|
||||||
current_empty = false;
|
current_empty = false;
|
||||||
any = true;
|
any = true;
|
||||||
}
|
}
|
||||||
@@ -624,11 +691,20 @@ fn emit_searchable_text(
|
|||||||
let size_mm = (run.height as f64 * map.scale / HELVETICA_CAP_RATIO).max(0.1);
|
let size_mm = (run.height as f64 * map.scale / HELVETICA_CAP_RATIO).max(0.1);
|
||||||
let deg = -(run.rotation as f64).to_degrees();
|
let deg = -(run.rotation as f64).to_degrees();
|
||||||
let rotate = if deg.abs() > 1e-6 {
|
let rotate = if deg.abs() > 1e-6 {
|
||||||
format!(" transform=\"rotate({} {} {})\"", fmt(deg), fmt(sx), fmt(sy))
|
format!(
|
||||||
|
" transform=\"rotate({} {} {})\"",
|
||||||
|
fmt(deg),
|
||||||
|
fmt(sx),
|
||||||
|
fmt(sy)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
let weight = if run.bold { " font-weight=\"bold\"" } else { "" };
|
let weight = if run.bold {
|
||||||
|
" font-weight=\"bold\""
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"<text x=\"{}\" y=\"{}\" font-size=\"{}\"{rotate}{weight} fill=\"{}\" font-family=\"{}\"{opacity}>{text}</text>\n",
|
"<text x=\"{}\" y=\"{}\" font-size=\"{}\"{rotate}{weight} fill=\"{}\" font-family=\"{}\"{opacity}>{text}</text>\n",
|
||||||
fmt(sx),
|
fmt(sx),
|
||||||
@@ -678,10 +754,15 @@ fn emit_image(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let to_svg = |high: [f32; 3], low: [f32; 3]| {
|
let to_svg = |high: [f32; 3], low: [f32; 3]| {
|
||||||
map.apply(high[0] as f64 + low[0] as f64 + ox, high[1] as f64 + low[1] as f64 + oy)
|
map.apply(
|
||||||
|
high[0] as f64 + low[0] as f64 + ox,
|
||||||
|
high[1] as f64 + low[1] as f64 + oy,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
let corners: [[f64; 2]; 4] =
|
let corners: [[f64; 2]; 4] = std::array::from_fn(|i| {
|
||||||
std::array::from_fn(|i| { let (x, y) = to_svg(image.corners[i], image.corners_low[i]); [x, y] });
|
let (x, y) = to_svg(image.corners[i], image.corners_low[i]);
|
||||||
|
[x, y]
|
||||||
|
});
|
||||||
if !corners.iter().flatten().all(|v| v.is_finite())
|
if !corners.iter().flatten().all(|v| v.is_finite())
|
||||||
|| !image.verts.iter().all(|v| {
|
|| !image.verts.iter().all(|v| {
|
||||||
let (x, y) = to_svg(v.pos, v.pos_low);
|
let (x, y) = to_svg(v.pos, v.pos_low);
|
||||||
@@ -739,12 +820,19 @@ fn emit_image(
|
|||||||
let mut d = String::new();
|
let mut d = String::new();
|
||||||
for (i, p) in ring.iter().enumerate() {
|
for (i, p) in ring.iter().enumerate() {
|
||||||
let (x, y) = map.apply(p[0] + ox, p[1] + oy);
|
let (x, y) = map.apply(p[0] + ox, p[1] + oy);
|
||||||
d.push_str(&format!("{} {} {} ", if i == 0 { 'M' } else { 'L' }, fmt(x), fmt(y)));
|
d.push_str(&format!(
|
||||||
|
"{} {} {} ",
|
||||||
|
if i == 0 { 'M' } else { 'L' },
|
||||||
|
fmt(x),
|
||||||
|
fmt(y)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
d.push_str("Z ");
|
d.push_str("Z ");
|
||||||
rings.push_str(&format!("<path d=\"{}\"/>\n", d.trim_end()));
|
rings.push_str(&format!("<path d=\"{}\"/>\n", d.trim_end()));
|
||||||
}
|
}
|
||||||
out.push_str(&format!("<clipPath id=\"img-clip-{id}\">{rings}</clipPath>\n"));
|
out.push_str(&format!(
|
||||||
|
"<clipPath id=\"img-clip-{id}\">{rings}</clipPath>\n"
|
||||||
|
));
|
||||||
clip_open = format!("<g clip-path=\"url(#img-clip-{id})\">\n");
|
clip_open = format!("<g clip-path=\"url(#img-clip-{id})\">\n");
|
||||||
clip_close = "</g>\n";
|
clip_close = "</g>\n";
|
||||||
}
|
}
|
||||||
@@ -907,7 +995,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn y_flip_puts_bottom_left_geometry_at_the_svg_bottom() {
|
fn y_flip_puts_bottom_left_geometry_at_the_svg_bottom() {
|
||||||
// A wire point at paper y=0 (sheet bottom) must land at y=paper_h in SVG.
|
// A wire point at paper y=0 (sheet bottom) must land at y=paper_h in SVG.
|
||||||
let map = PageMap { paper_w: 210.0, paper_h: 297.0, scale: 1.0, rotation_deg: 0 };
|
let map = PageMap {
|
||||||
|
paper_w: 210.0,
|
||||||
|
paper_h: 297.0,
|
||||||
|
scale: 1.0,
|
||||||
|
rotation_deg: 0,
|
||||||
|
};
|
||||||
assert_eq!(map.apply(10.0, 0.0), (10.0, 297.0));
|
assert_eq!(map.apply(10.0, 0.0), (10.0, 297.0));
|
||||||
assert_eq!(map.apply(0.0, 297.0), (0.0, 0.0));
|
assert_eq!(map.apply(0.0, 297.0), (0.0, 0.0));
|
||||||
}
|
}
|
||||||
@@ -915,12 +1008,22 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn rotation_matches_the_pdf_ctm_convention() {
|
fn rotation_matches_the_pdf_ctm_convention() {
|
||||||
// PDF 90°: x' = paper_w - s*y, y' = s*x, then SVG flips y.
|
// PDF 90°: x' = paper_w - s*y, y' = s*x, then SVG flips y.
|
||||||
let map = PageMap { paper_w: 297.0, paper_h: 210.0, scale: 1.0, rotation_deg: 90 };
|
let map = PageMap {
|
||||||
|
paper_w: 297.0,
|
||||||
|
paper_h: 210.0,
|
||||||
|
scale: 1.0,
|
||||||
|
rotation_deg: 90,
|
||||||
|
};
|
||||||
let (x, y) = map.apply(0.0, 0.0);
|
let (x, y) = map.apply(0.0, 0.0);
|
||||||
assert_eq!((x, y), (297.0, 210.0));
|
assert_eq!((x, y), (297.0, 210.0));
|
||||||
let (x, y) = map.apply(5.0, 7.0);
|
let (x, y) = map.apply(5.0, 7.0);
|
||||||
assert_eq!((x, y), (297.0 - 7.0, 210.0 - 5.0));
|
assert_eq!((x, y), (297.0 - 7.0, 210.0 - 5.0));
|
||||||
let map = PageMap { paper_w: 210.0, paper_h: 297.0, scale: 1.0, rotation_deg: 180 };
|
let map = PageMap {
|
||||||
|
paper_w: 210.0,
|
||||||
|
paper_h: 297.0,
|
||||||
|
scale: 1.0,
|
||||||
|
rotation_deg: 180,
|
||||||
|
};
|
||||||
let (x, y) = map.apply(0.0, 0.0);
|
let (x, y) = map.apply(0.0, 0.0);
|
||||||
assert_eq!((x, y), (210.0, 0.0));
|
assert_eq!((x, y), (210.0, 0.0));
|
||||||
}
|
}
|
||||||
@@ -950,7 +1053,12 @@ mod tests {
|
|||||||
fn nan_splits_subpaths_and_helpers_stay_paper_coloured() {
|
fn nan_splits_subpaths_and_helpers_stay_paper_coloured() {
|
||||||
let wire = WireModel::solid(
|
let wire = WireModel::solid(
|
||||||
"L1".to_string(),
|
"L1".to_string(),
|
||||||
vec![[0.0, 0.0, 0.0], [f32::NAN, f32::NAN, 0.0], [5.0, 5.0, 0.0], [6.0, 5.0, 0.0]],
|
vec![
|
||||||
|
[0.0, 0.0, 0.0],
|
||||||
|
[f32::NAN, f32::NAN, 0.0],
|
||||||
|
[5.0, 5.0, 0.0],
|
||||||
|
[6.0, 5.0, 0.0],
|
||||||
|
],
|
||||||
[0.0, 0.0, 1.0, 1.0],
|
[0.0, 0.0, 1.0, 1.0],
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@@ -1002,6 +1110,9 @@ mod tests {
|
|||||||
let svg = build_svg(&page, None).expect("builds");
|
let svg = build_svg(&page, None).expect("builds");
|
||||||
assert!(svg.contains("<clipPath id=\"plot-clip\">"), "{svg}");
|
assert!(svg.contains("<clipPath id=\"plot-clip\">"), "{svg}");
|
||||||
// y-flipped: rect y = 297 - (20 + 50) = 227.
|
// y-flipped: rect y = 297 - (20 + 50) = 227.
|
||||||
assert!(svg.contains("x=\"10\" y=\"227\" width=\"100\" height=\"50\""), "{svg}");
|
assert!(
|
||||||
|
svg.contains("x=\"10\" y=\"227\" width=\"100\" height=\"50\""),
|
||||||
|
"{svg}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+28
-5
@@ -10,7 +10,19 @@ use codec::types::Color;
|
|||||||
/// (name, type, face lighting model, face lighting quality, face color
|
/// (name, type, face lighting model, face lighting quality, face color
|
||||||
/// mode, face modifier, edge model, edge style, internal only, extended
|
/// mode, face modifier, edge model, edge style, internal only, extended
|
||||||
/// lighting model, properties)
|
/// lighting model, properties)
|
||||||
type Row = (&'static str, i16, i16, i16, i16, i32, i32, i32, bool, i16, &'static str);
|
type Row = (
|
||||||
|
&'static str,
|
||||||
|
i16,
|
||||||
|
i16,
|
||||||
|
i16,
|
||||||
|
i16,
|
||||||
|
i32,
|
||||||
|
i32,
|
||||||
|
i32,
|
||||||
|
bool,
|
||||||
|
i16,
|
||||||
|
&'static str,
|
||||||
|
);
|
||||||
|
|
||||||
const STYLES: &[Row] = &[
|
const STYLES: &[Row] = &[
|
||||||
("2dWireframe", 4, 0, 2, 0, 0, 1, 4, false, 3,
|
("2dWireframe", 4, 0, 2, 0, 0, 1, 4, false, 3,
|
||||||
@@ -103,7 +115,9 @@ pub fn populate_document(document: &mut codec::CadDocument) {
|
|||||||
if present {
|
if present {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(properties) = props.split(' ').map(property).collect::<Option<Vec<_>>>() else { continue };
|
let Some(properties) = props.split(' ').map(property).collect::<Option<Vec<_>>>() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let handle = document.allocate_handle();
|
let handle = document.allocate_handle();
|
||||||
let mut style = VisualStyle::new();
|
let mut style = VisualStyle::new();
|
||||||
style.handle = handle;
|
style.handle = handle;
|
||||||
@@ -120,7 +134,9 @@ pub fn populate_document(document: &mut codec::CadDocument) {
|
|||||||
style.internal_use_only = internal;
|
style.internal_use_only = internal;
|
||||||
style.extended_lighting_model = elm;
|
style.extended_lighting_model = elm;
|
||||||
style.properties = properties;
|
style.properties = properties;
|
||||||
document.objects.insert(handle, ObjectType::VisualStyle(style));
|
document
|
||||||
|
.objects
|
||||||
|
.insert(handle, ObjectType::VisualStyle(style));
|
||||||
if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&dictionary) {
|
if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&dictionary) {
|
||||||
entries.add_entry(name, handle);
|
entries.add_entry(name, handle);
|
||||||
}
|
}
|
||||||
@@ -130,7 +146,10 @@ pub fn populate_document(document: &mut codec::CadDocument) {
|
|||||||
/// The visual styles offered by name (not the internal ones), as the
|
/// The visual styles offered by name (not the internal ones), as the
|
||||||
/// reference lists them: the 2D wireframe style shows as "2D Wireframe".
|
/// reference lists them: the 2D wireframe style shows as "2D Wireframe".
|
||||||
pub fn offered(document: &codec::CadDocument) -> Vec<(String, codec::types::Handle)> {
|
pub fn offered(document: &codec::CadDocument) -> Vec<(String, codec::types::Handle)> {
|
||||||
let Some(ObjectType::Dictionary(entries)) = document.objects.get(&document.header.acad_visualstyle_dict_handle) else {
|
let Some(ObjectType::Dictionary(entries)) = document
|
||||||
|
.objects
|
||||||
|
.get(&document.header.acad_visualstyle_dict_handle)
|
||||||
|
else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
entries
|
entries
|
||||||
@@ -141,7 +160,11 @@ pub fn offered(document: &codec::CadDocument) -> Vec<(String, codec::types::Hand
|
|||||||
_ => false,
|
_ => false,
|
||||||
})
|
})
|
||||||
.map(|(name, handle)| {
|
.map(|(name, handle)| {
|
||||||
let shown = if name.eq_ignore_ascii_case("2dWireframe") { "2D Wireframe".to_string() } else { name.clone() };
|
let shown = if name.eq_ignore_ascii_case("2dWireframe") {
|
||||||
|
"2D Wireframe".to_string()
|
||||||
|
} else {
|
||||||
|
name.clone()
|
||||||
|
};
|
||||||
(shown, *handle)
|
(shown, *handle)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
+9
-11
@@ -159,8 +159,6 @@ pub async fn snapshot(name: &str, bytes: &[u8], thumbnail: Option<&[u8]>) -> Res
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub async fn read(name: &str) -> Result<Vec<u8>, String> {
|
pub async fn read(name: &str) -> Result<Vec<u8>, String> {
|
||||||
let directory = autosave_directory(false).await?;
|
let directory = autosave_directory(false).await?;
|
||||||
@@ -250,9 +248,7 @@ fn thumbnail_key(name: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
async fn read_index(
|
async fn read_index(directory: &web_sys::FileSystemDirectoryHandle) -> Result<Vec<String>, String> {
|
||||||
directory: &web_sys::FileSystemDirectoryHandle,
|
|
||||||
) -> Result<Vec<String>, String> {
|
|
||||||
let bytes = match super::web_recent::read_entry(directory, INDEX_KEY).await {
|
let bytes = match super::web_recent::read_entry(directory, INDEX_KEY).await {
|
||||||
Ok(bytes) => bytes,
|
Ok(bytes) => bytes,
|
||||||
// A missing index is a fresh store.
|
// A missing index is a fresh store.
|
||||||
@@ -317,7 +313,10 @@ mod tests {
|
|||||||
// empty-stem fallback reinstated the unsanitized input, breaking
|
// empty-stem fallback reinstated the unsanitized input, breaking
|
||||||
// idempotence across crash cycles.
|
// idempotence across crash cycles.
|
||||||
let once = recovered_name("a.b/c");
|
let once = recovered_name("a.b/c");
|
||||||
assert_eq!(once, "c (recovered).dxf", "final path component, no separator survives");
|
assert_eq!(
|
||||||
|
once, "c (recovered).dxf",
|
||||||
|
"final path component, no separator survives"
|
||||||
|
);
|
||||||
assert_eq!(recovered_name(&once), once, "idempotent");
|
assert_eq!(recovered_name(&once), once, "idempotent");
|
||||||
let dotfile = recovered_name("/.dwg");
|
let dotfile = recovered_name("/.dwg");
|
||||||
assert_eq!(dotfile, ".dwg (recovered).dwg");
|
assert_eq!(dotfile, ".dwg (recovered).dwg");
|
||||||
@@ -355,10 +354,7 @@ mod tests {
|
|||||||
"plan (recovered).dwg"
|
"plan (recovered).dwg"
|
||||||
);
|
);
|
||||||
// Extensionless stacked names de-stack too, then gain ".dxf".
|
// Extensionless stacked names de-stack too, then gain ".dxf".
|
||||||
assert_eq!(
|
assert_eq!(recovered_name("plan (recovered)"), "plan (recovered).dxf");
|
||||||
recovered_name("plan (recovered)"),
|
|
||||||
"plan (recovered).dxf"
|
|
||||||
);
|
|
||||||
// A marker embedded in the middle (not a suffix) is left alone.
|
// A marker embedded in the middle (not a suffix) is left alone.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
recovered_name("(recovered) plan.dwg"),
|
recovered_name("(recovered) plan.dwg"),
|
||||||
@@ -407,7 +403,9 @@ mod tests {
|
|||||||
];
|
];
|
||||||
let plan = restore_all_plan(&entries);
|
let plan = restore_all_plan(&entries);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
plan.iter().map(|(name, _)| name.as_str()).collect::<Vec<_>>(),
|
plan.iter()
|
||||||
|
.map(|(name, _)| name.as_str())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
[
|
[
|
||||||
"new (recovered).dwg",
|
"new (recovered).dwg",
|
||||||
"mid (recovered).dwg",
|
"mid (recovered).dwg",
|
||||||
|
|||||||
+11
-9
@@ -174,9 +174,8 @@ fn percent_decode(value: &str) -> String {
|
|||||||
let mut out = Vec::with_capacity(bytes.len());
|
let mut out = Vec::with_capacity(bytes.len());
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
while index < bytes.len() {
|
while index < bytes.len() {
|
||||||
let escape = (bytes[index] == b'%' && index + 2 < bytes.len()).then(|| {
|
let escape = (bytes[index] == b'%' && index + 2 < bytes.len())
|
||||||
(hex(bytes[index + 1]), hex(bytes[index + 2]))
|
.then(|| (hex(bytes[index + 1]), hex(bytes[index + 2])));
|
||||||
});
|
|
||||||
match escape {
|
match escape {
|
||||||
Some((Some(high), Some(low))) => {
|
Some((Some(high), Some(low))) => {
|
||||||
out.push(high * 16 + low);
|
out.push(high * 16 + low);
|
||||||
@@ -248,7 +247,10 @@ fn enqueue_or_full(name: String, bytes: Vec<u8>) -> LinkOutcome {
|
|||||||
/// built inline in the poll arm and mention images); the classification
|
/// built inline in the poll arm and mention images); the classification
|
||||||
/// itself is shared via `RemoteFetchError`.
|
/// itself is shared via `RemoteFetchError`.
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
fn fetch_error_text(url: &str, error: crate::scene::model::image_model::RemoteFetchError) -> String {
|
fn fetch_error_text(
|
||||||
|
url: &str,
|
||||||
|
error: crate::scene::model::image_model::RemoteFetchError,
|
||||||
|
) -> String {
|
||||||
use crate::scene::model::image_model::RemoteFetchError;
|
use crate::scene::model::image_model::RemoteFetchError;
|
||||||
match error {
|
match error {
|
||||||
RemoteFetchError::Network => format!(
|
RemoteFetchError::Network => format!(
|
||||||
@@ -402,7 +404,10 @@ mod tests {
|
|||||||
"?file=https%3A%2F%2Fexample.com%2F..dwgx",
|
"?file=https%3A%2F%2Fexample.com%2F..dwgx",
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(parse_link(search), ParsedLink::Ignored(LinkRefusal::NotDrawing, _)),
|
matches!(
|
||||||
|
parse_link(search),
|
||||||
|
ParsedLink::Ignored(LinkRefusal::NotDrawing, _)
|
||||||
|
),
|
||||||
"{search} must be refused as not a drawing"
|
"{search} must be refused as not a drawing"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -461,10 +466,7 @@ mod tests {
|
|||||||
fn scheme_prefix_matches_rfc3986_shape_only() {
|
fn scheme_prefix_matches_rfc3986_shape_only() {
|
||||||
assert_eq!(scheme_prefix("https://x"), Some("https".to_string()));
|
assert_eq!(scheme_prefix("https://x"), Some("https".to_string()));
|
||||||
assert_eq!(scheme_prefix("c:\\x"), Some("c".to_string()));
|
assert_eq!(scheme_prefix("c:\\x"), Some("c".to_string()));
|
||||||
assert_eq!(
|
assert_eq!(scheme_prefix("x1+2-3.4:rest"), Some("x1+2-3.4".to_string()));
|
||||||
scheme_prefix("x1+2-3.4:rest"),
|
|
||||||
Some("x1+2-3.4".to_string())
|
|
||||||
);
|
|
||||||
// A colon after a non-scheme character (space, slash, digit start)
|
// A colon after a non-scheme character (space, slash, digit start)
|
||||||
// is not a scheme.
|
// is not a scheme.
|
||||||
assert_eq!(scheme_prefix("plan.dwg"), None);
|
assert_eq!(scheme_prefix("plan.dwg"), None);
|
||||||
|
|||||||
+9
-17
@@ -53,7 +53,8 @@ pub fn persist(tabs: &[String], active: usize) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let session = WebSession::new(tabs.to_vec(), active);
|
let session = WebSession::new(tabs.to_vec(), active);
|
||||||
if let Some(storage) = web_sys::window().and_then(|window| window.local_storage().ok().flatten())
|
if let Some(storage) =
|
||||||
|
web_sys::window().and_then(|window| window.local_storage().ok().flatten())
|
||||||
{
|
{
|
||||||
let _ = storage.set_item(SESSION_KEY, &session.to_json());
|
let _ = storage.set_item(SESSION_KEY, &session.to_json());
|
||||||
}
|
}
|
||||||
@@ -76,10 +77,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_session_round_trips_through_json() {
|
fn a_session_round_trips_through_json() {
|
||||||
let session = WebSession::new(
|
let session = WebSession::new(vec!["plan.dwg".to_string(), "sketch.dxf".to_string()], 1);
|
||||||
vec!["plan.dwg".to_string(), "sketch.dxf".to_string()],
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
let restored = WebSession::from_json(&session.to_json()).expect("must parse");
|
let restored = WebSession::from_json(&session.to_json()).expect("must parse");
|
||||||
assert_eq!(restored, session);
|
assert_eq!(restored, session);
|
||||||
assert_eq!(restored.tabs, ["plan.dwg", "sketch.dxf"]);
|
assert_eq!(restored.tabs, ["plan.dwg", "sketch.dxf"]);
|
||||||
@@ -91,8 +89,8 @@ mod tests {
|
|||||||
// Construction clamps…
|
// Construction clamps…
|
||||||
assert_eq!(WebSession::new(vec!["a.dwg".to_string()], 9).active, 0);
|
assert_eq!(WebSession::new(vec!["a.dwg".to_string()], 9).active, 0);
|
||||||
// …and so does a parse of stale or hostile JSON, both directions.
|
// …and so does a parse of stale or hostile JSON, both directions.
|
||||||
let over = WebSession::from_json(r#"{"tabs":["a.dwg","b.dwg"],"active":99}"#)
|
let over =
|
||||||
.expect("must parse");
|
WebSession::from_json(r#"{"tabs":["a.dwg","b.dwg"],"active":99}"#).expect("must parse");
|
||||||
assert_eq!(over.active, 1);
|
assert_eq!(over.active, 1);
|
||||||
let under = WebSession::from_json(r#"{"tabs":["a.dwg"],"active":0}"#).unwrap();
|
let under = WebSession::from_json(r#"{"tabs":["a.dwg"],"active":0}"#).unwrap();
|
||||||
assert_eq!(under.active, 0);
|
assert_eq!(under.active, 0);
|
||||||
@@ -103,8 +101,8 @@ mod tests {
|
|||||||
assert!(WebSession::from_json("").is_none());
|
assert!(WebSession::from_json("").is_none());
|
||||||
assert!(WebSession::from_json("not json").is_none());
|
assert!(WebSession::from_json("not json").is_none());
|
||||||
assert!(WebSession::from_json("{}").is_none()); // missing `tabs` field
|
assert!(WebSession::from_json("{}").is_none()); // missing `tabs` field
|
||||||
// A session whose every tab closed is still valid memory (it says
|
// A session whose every tab closed is still valid memory (it says
|
||||||
// the crash lost nothing that was visibly open).
|
// the crash lost nothing that was visibly open).
|
||||||
let empty = WebSession::from_json(r#"{"tabs":[],"active":0}"#).unwrap();
|
let empty = WebSession::from_json(r#"{"tabs":[],"active":0}"#).unwrap();
|
||||||
assert!(empty.tabs.is_empty());
|
assert!(empty.tabs.is_empty());
|
||||||
assert_eq!(empty.active, 0);
|
assert_eq!(empty.active, 0);
|
||||||
@@ -114,13 +112,7 @@ mod tests {
|
|||||||
fn duplicate_names_are_kept_as_is() {
|
fn duplicate_names_are_kept_as_is() {
|
||||||
// Two tabs may share a display name on the web; the memory list
|
// Two tabs may share a display name on the web; the memory list
|
||||||
// records the list as it was, de-duplication is not its job.
|
// records the list as it was, de-duplication is not its job.
|
||||||
let session = WebSession::new(
|
let session = WebSession::new(vec!["a.dwg".to_string(), "a.dwg".to_string()], 1);
|
||||||
vec!["a.dwg".to_string(), "a.dwg".to_string()],
|
assert_eq!(WebSession::from_json(&session.to_json()).unwrap(), session);
|
||||||
1,
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
WebSession::from_json(&session.to_json()).unwrap(),
|
|
||||||
session
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,11 +212,9 @@ pub(super) async fn serialize_document(encoded_request: Vec<u8>) -> Result<Vec<u
|
|||||||
async fn serialize_document_via_worker(encoded_request: &[u8]) -> Result<Vec<u8>, String> {
|
async fn serialize_document_via_worker(encoded_request: &[u8]) -> Result<Vec<u8>, String> {
|
||||||
let options = WorkerOptions::new();
|
let options = WorkerOptions::new();
|
||||||
options.set_type(WorkerType::Module);
|
options.set_type(WorkerType::Module);
|
||||||
let worker =
|
let worker = Worker::new_with_options(WORKER_URL, &options).map_err(|error| js_error(error))?;
|
||||||
Worker::new_with_options(WORKER_URL, &options).map_err(|error| js_error(error))?;
|
|
||||||
|
|
||||||
let (sender, receiver) =
|
let (sender, receiver) = iced::futures::channel::oneshot::channel::<Result<Vec<u8>, String>>();
|
||||||
iced::futures::channel::oneshot::channel::<Result<Vec<u8>, String>>();
|
|
||||||
let sender = Rc::new(RefCell::new(Some(sender)));
|
let sender = Rc::new(RefCell::new(Some(sender)));
|
||||||
let message_sender = sender.clone();
|
let message_sender = sender.clone();
|
||||||
let on_message = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
|
let on_message = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
|
||||||
|
|||||||
@@ -85,7 +85,12 @@ impl CadCommand for PasteSpecCommand {
|
|||||||
if self.placing_text {
|
if self.placing_text {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
match text.trim().trim_start_matches('_').to_ascii_uppercase().as_str() {
|
match text
|
||||||
|
.trim()
|
||||||
|
.trim_start_matches('_')
|
||||||
|
.to_ascii_uppercase()
|
||||||
|
.as_str()
|
||||||
|
{
|
||||||
"O" | "OBJECTS" if self.objects => Some(CmdResult::Dispatch("PASTECLIP".into())),
|
"O" | "OBJECTS" if self.objects => Some(CmdResult::Dispatch("PASTECLIP".into())),
|
||||||
"B" | "BLOCK" if self.objects => Some(CmdResult::Dispatch("PASTEBLOCK".into())),
|
"B" | "BLOCK" if self.objects => Some(CmdResult::Dispatch("PASTEBLOCK".into())),
|
||||||
"T" | "TEXT" if self.text.is_some() => {
|
"T" | "TEXT" if self.text.is_some() => {
|
||||||
@@ -123,7 +128,9 @@ impl CadCommand for PasteSpecCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["PASTESPEC"] });
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
|
names: &["PASTESPEC"]
|
||||||
|
});
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
@@ -739,8 +739,12 @@ pub fn solve_line_line_ccp(
|
|||||||
candidates
|
candidates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.min_by(|(t1_a, t2_a, r_a), (t1_b, t2_b, r_b)| {
|
.min_by(|(t1_a, t2_a, r_a), (t1_b, t2_b, r_b)| {
|
||||||
let dist_a = (*t1_a - hint1).length_squared() + (*t2_a - hint2).length_squared() + if *r_a > 50000.0 { 1e8 } else { 0.0 };
|
let dist_a = (*t1_a - hint1).length_squared()
|
||||||
let dist_b = (*t1_b - hint1).length_squared() + (*t2_b - hint2).length_squared() + if *r_b > 50000.0 { 1e8 } else { 0.0 };
|
+ (*t2_a - hint2).length_squared()
|
||||||
|
+ if *r_a > 50000.0 { 1e8 } else { 0.0 };
|
||||||
|
let dist_b = (*t1_b - hint1).length_squared()
|
||||||
|
+ (*t2_b - hint2).length_squared()
|
||||||
|
+ if *r_b > 50000.0 { 1e8 } else { 0.0 };
|
||||||
dist_a.total_cmp(&dist_b)
|
dist_a.total_cmp(&dist_b)
|
||||||
})
|
})
|
||||||
.map(|(t1, t2, _)| (t1, t2))
|
.map(|(t1, t2, _)| (t1, t2))
|
||||||
@@ -790,8 +794,12 @@ pub fn solve_circle_line_ccp(
|
|||||||
let t_a = i as f64 * dt;
|
let t_a = i as f64 * dt;
|
||||||
let t_b = (i + 1) as f64 * dt;
|
let t_b = (i + 1) as f64 * dt;
|
||||||
|
|
||||||
let Some((res_a, _, _, _)) = eval_t(t_a) else { continue; };
|
let Some((res_a, _, _, _)) = eval_t(t_a) else {
|
||||||
let Some((res_b, _, _, _)) = eval_t(t_b) else { continue; };
|
continue;
|
||||||
|
};
|
||||||
|
let Some((res_b, _, _, _)) = eval_t(t_b) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
if res_a.signum() != res_b.signum() {
|
if res_a.signum() != res_b.signum() {
|
||||||
let mut lo = t_a;
|
let mut lo = t_a;
|
||||||
@@ -799,7 +807,9 @@ pub fn solve_circle_line_ccp(
|
|||||||
let mut best_t = (lo + hi) * 0.5;
|
let mut best_t = (lo + hi) * 0.5;
|
||||||
for _ in 0..16 {
|
for _ in 0..16 {
|
||||||
let mid = (lo + hi) * 0.5;
|
let mid = (lo + hi) * 0.5;
|
||||||
let Some((res_m, _, _, _)) = eval_t(mid) else { break; };
|
let Some((res_m, _, _, _)) = eval_t(mid) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
best_t = mid;
|
best_t = mid;
|
||||||
if res_m.abs() < 1e-8 {
|
if res_m.abs() < 1e-8 {
|
||||||
break;
|
break;
|
||||||
@@ -822,8 +832,12 @@ pub fn solve_circle_line_ccp(
|
|||||||
candidates
|
candidates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.min_by(|(c_a, l_a, r_a), (c_b, l_b, r_b)| {
|
.min_by(|(c_a, l_a, r_a), (c_b, l_b, r_b)| {
|
||||||
let dist_a = (*c_a - hint_c).length_squared() + (*l_a - hint_l).length_squared() + if *r_a > 50000.0 { 1e8 } else { 0.0 };
|
let dist_a = (*c_a - hint_c).length_squared()
|
||||||
let dist_b = (*c_b - hint_c).length_squared() + (*l_b - hint_l).length_squared() + if *r_b > 50000.0 { 1e8 } else { 0.0 };
|
+ (*l_a - hint_l).length_squared()
|
||||||
|
+ if *r_a > 50000.0 { 1e8 } else { 0.0 };
|
||||||
|
let dist_b = (*c_b - hint_c).length_squared()
|
||||||
|
+ (*l_b - hint_l).length_squared()
|
||||||
|
+ if *r_b > 50000.0 { 1e8 } else { 0.0 };
|
||||||
dist_a.total_cmp(&dist_b)
|
dist_a.total_cmp(&dist_b)
|
||||||
})
|
})
|
||||||
.map(|(pt_c, pt_l, _)| (pt_c, pt_l))
|
.map(|(pt_c, pt_l, _)| (pt_c, pt_l))
|
||||||
@@ -873,8 +887,12 @@ pub fn solve_ellipse_line_ccp(
|
|||||||
let t_a = i as f64 * dt;
|
let t_a = i as f64 * dt;
|
||||||
let t_b = (i + 1) as f64 * dt;
|
let t_b = (i + 1) as f64 * dt;
|
||||||
|
|
||||||
let Some((res_a, _, _, _)) = eval_t(t_a) else { continue; };
|
let Some((res_a, _, _, _)) = eval_t(t_a) else {
|
||||||
let Some((res_b, _, _, _)) = eval_t(t_b) else { continue; };
|
continue;
|
||||||
|
};
|
||||||
|
let Some((res_b, _, _, _)) = eval_t(t_b) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
if res_a.signum() != res_b.signum() {
|
if res_a.signum() != res_b.signum() {
|
||||||
let mut lo = t_a;
|
let mut lo = t_a;
|
||||||
@@ -882,7 +900,9 @@ pub fn solve_ellipse_line_ccp(
|
|||||||
let mut best_t = (lo + hi) * 0.5;
|
let mut best_t = (lo + hi) * 0.5;
|
||||||
for _ in 0..16 {
|
for _ in 0..16 {
|
||||||
let mid = (lo + hi) * 0.5;
|
let mid = (lo + hi) * 0.5;
|
||||||
let Some((res_m, _, _, _)) = eval_t(mid) else { break; };
|
let Some((res_m, _, _, _)) = eval_t(mid) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
best_t = mid;
|
best_t = mid;
|
||||||
if res_m.abs() < 1e-8 {
|
if res_m.abs() < 1e-8 {
|
||||||
break;
|
break;
|
||||||
@@ -905,8 +925,12 @@ pub fn solve_ellipse_line_ccp(
|
|||||||
candidates
|
candidates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.min_by(|(e_a, l_a, r_a), (e_b, l_b, r_b)| {
|
.min_by(|(e_a, l_a, r_a), (e_b, l_b, r_b)| {
|
||||||
let dist_a = (*e_a - hint_e).length_squared() + (*l_a - hint_l).length_squared() + if *r_a > 50000.0 { 1e8 } else { 0.0 };
|
let dist_a = (*e_a - hint_e).length_squared()
|
||||||
let dist_b = (*e_b - hint_e).length_squared() + (*l_b - hint_l).length_squared() + if *r_b > 50000.0 { 1e8 } else { 0.0 };
|
+ (*l_a - hint_l).length_squared()
|
||||||
|
+ if *r_a > 50000.0 { 1e8 } else { 0.0 };
|
||||||
|
let dist_b = (*e_b - hint_e).length_squared()
|
||||||
|
+ (*l_b - hint_l).length_squared()
|
||||||
|
+ if *r_b > 50000.0 { 1e8 } else { 0.0 };
|
||||||
dist_a.total_cmp(&dist_b)
|
dist_a.total_cmp(&dist_b)
|
||||||
})
|
})
|
||||||
.map(|(pt_e, pt_l, _)| (pt_e, pt_l))
|
.map(|(pt_e, pt_l, _)| (pt_e, pt_l))
|
||||||
@@ -4140,7 +4164,10 @@ mod acad_flow_tests {
|
|||||||
let preview = cmd.on_preview_wires_with_tangent(DVec3::new(2.0, 2.0, 0.0), None);
|
let preview = cmd.on_preview_wires_with_tangent(DVec3::new(2.0, 2.0, 0.0), None);
|
||||||
assert!(!preview.is_empty(), "Live preview must generate arc wire");
|
assert!(!preview.is_empty(), "Live preview must generate arc wire");
|
||||||
assert!(
|
assert!(
|
||||||
preview[0].tangent_geoms.iter().any(|g| matches!(g, TangentGeom::Arc { .. })),
|
preview[0]
|
||||||
|
.tangent_geoms
|
||||||
|
.iter()
|
||||||
|
.any(|g| matches!(g, TangentGeom::Arc { .. })),
|
||||||
"Preview must produce an analytical arc geometry"
|
"Preview must produce an analytical arc geometry"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4178,7 +4205,10 @@ mod acad_flow_tests {
|
|||||||
let preview = cmd.on_preview_wires_with_tangent(DVec3::new(1.0, 0.0, 0.0), Some(line));
|
let preview = cmd.on_preview_wires_with_tangent(DVec3::new(1.0, 0.0, 0.0), Some(line));
|
||||||
assert!(!preview.is_empty(), "Live preview must generate arc wire");
|
assert!(!preview.is_empty(), "Live preview must generate arc wire");
|
||||||
assert!(
|
assert!(
|
||||||
preview[0].tangent_geoms.iter().any(|g| matches!(g, TangentGeom::Arc { .. })),
|
preview[0]
|
||||||
|
.tangent_geoms
|
||||||
|
.iter()
|
||||||
|
.any(|g| matches!(g, TangentGeom::Arc { .. })),
|
||||||
"Preview must produce an analytical arc geometry"
|
"Preview must produce an analytical arc geometry"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4212,7 +4242,10 @@ mod acad_flow_tests {
|
|||||||
let preview = cmd.on_preview_wires_with_tangent(DVec3::new(2.0, 2.0, 0.0), None);
|
let preview = cmd.on_preview_wires_with_tangent(DVec3::new(2.0, 2.0, 0.0), None);
|
||||||
assert!(!preview.is_empty(), "Live preview must exist");
|
assert!(!preview.is_empty(), "Live preview must exist");
|
||||||
assert!(
|
assert!(
|
||||||
preview[0].tangent_geoms.iter().any(|g| matches!(g, TangentGeom::Arc { .. })),
|
preview[0]
|
||||||
|
.tangent_geoms
|
||||||
|
.iter()
|
||||||
|
.any(|g| matches!(g, TangentGeom::Arc { .. })),
|
||||||
"Preview must produce an arc, not a straight fallback line"
|
"Preview must produce an arc, not a straight fallback line"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4240,7 +4273,10 @@ mod acad_flow_tests {
|
|||||||
let preview = cmd.on_preview_wires_with_tangent(DVec3::new(1.0, 0.0, 0.0), Some(line));
|
let preview = cmd.on_preview_wires_with_tangent(DVec3::new(1.0, 0.0, 0.0), Some(line));
|
||||||
assert!(!preview.is_empty());
|
assert!(!preview.is_empty());
|
||||||
assert!(
|
assert!(
|
||||||
preview[0].tangent_geoms.iter().any(|g| matches!(g, TangentGeom::Arc { .. })),
|
preview[0]
|
||||||
|
.tangent_geoms
|
||||||
|
.iter()
|
||||||
|
.any(|g| matches!(g, TangentGeom::Arc { .. })),
|
||||||
"Preview must produce an arc, not a straight fallback line"
|
"Preview must produce an arc, not a straight fallback line"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4266,7 +4302,10 @@ mod acad_flow_tests {
|
|||||||
// Pick 1: Tangent on Line 1 (y = 0)
|
// Pick 1: Tangent on Line 1 (y = 0)
|
||||||
cmd.on_point_with_tangent(DVec3::new(2.0, 0.0, 0.0), Some(line1));
|
cmd.on_point_with_tangent(DVec3::new(2.0, 0.0, 0.0), Some(line1));
|
||||||
// Pick 2: Point in first quadrant
|
// Pick 2: Point in first quadrant
|
||||||
cmd.on_point_with_tangent(DVec3::new(1.0 - (0.5_f64).sqrt(), 1.0 - (0.5_f64).sqrt(), 0.0), None);
|
cmd.on_point_with_tangent(
|
||||||
|
DVec3::new(1.0 - (0.5_f64).sqrt(), 1.0 - (0.5_f64).sqrt(), 0.0),
|
||||||
|
None,
|
||||||
|
);
|
||||||
// Pick 3: Tangent on Line 2 (x = 0)
|
// Pick 3: Tangent on Line 2 (x = 0)
|
||||||
let r3 = cmd.on_point_with_tangent(DVec3::new(0.0, 2.0, 0.0), Some(line2));
|
let r3 = cmd.on_point_with_tangent(DVec3::new(0.0, 2.0, 0.0), Some(line2));
|
||||||
let arc = committed_arc(r3.expect("two-line arc command must commit"));
|
let arc = committed_arc(r3.expect("two-line arc command must commit"));
|
||||||
|
|||||||
@@ -264,7 +264,9 @@ impl HatchCommand {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finish_selected(mut self) -> CmdResult { self.on_enter() }
|
pub fn finish_selected(mut self) -> CmdResult {
|
||||||
|
self.on_enter()
|
||||||
|
}
|
||||||
|
|
||||||
fn set_object_selection(&mut self, handles: Vec<Handle>) {
|
fn set_object_selection(&mut self, handles: Vec<Handle>) {
|
||||||
let mut segments = Vec::new();
|
let mut segments = Vec::new();
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ impl CadCommand for DistCommand {
|
|||||||
let ctx = unit_context();
|
let ctx = unit_context();
|
||||||
let sign = if ctx.angdir_cw { -1.0 } else { 1.0 };
|
let sign = if ctx.angdir_cw { -1.0 } else { 1.0 };
|
||||||
let turn = std::f64::consts::TAU;
|
let turn = std::f64::consts::TAU;
|
||||||
let angle_xy = (sign * (dy.atan2(dx) - ctx.angbase)).rem_euclid(turn).to_degrees();
|
let angle_xy = (sign * (dy.atan2(dx) - ctx.angbase))
|
||||||
|
.rem_euclid(turn)
|
||||||
|
.to_degrees();
|
||||||
// Angle from XY plane toward Z (elevation angle)
|
// Angle from XY plane toward Z (elevation angle)
|
||||||
let dist_xy = dx.hypot(dy);
|
let dist_xy = dx.hypot(dy);
|
||||||
let angle_z = dz.atan2(dist_xy).to_degrees();
|
let angle_z = dz.atan2(dist_xy).to_degrees();
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ fn associative_option(on: bool) -> crate::command::CmdOption {
|
|||||||
|
|
||||||
/// Typed `AS` / `ASSOCIATIVE` flips the Associative option.
|
/// Typed `AS` / `ASSOCIATIVE` flips the Associative option.
|
||||||
fn is_associative_keyword(text: &str) -> bool {
|
fn is_associative_keyword(text: &str) -> bool {
|
||||||
matches!(text.trim().to_ascii_uppercase().as_str(), "AS" | "ASSOCIATIVE" | "_AS")
|
matches!(
|
||||||
|
text.trim().to_ascii_uppercase().as_str(),
|
||||||
|
"AS" | "ASSOCIATIVE" | "_AS"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Associative arrays are built in world axes; a rotated working plane
|
/// Associative arrays are built in world axes; a rotated working plane
|
||||||
|
|||||||
@@ -56,7 +56,12 @@ impl CadCommand for ArrayEditCommand {
|
|||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
let array = self.array?;
|
let array = self.array?;
|
||||||
match text.trim().trim_start_matches('_').to_ascii_uppercase().as_str() {
|
match text
|
||||||
|
.trim()
|
||||||
|
.trim_start_matches('_')
|
||||||
|
.to_ascii_uppercase()
|
||||||
|
.as_str()
|
||||||
|
{
|
||||||
"S" | "SOURCE" => Some(CmdResult::Dispatch(format!(
|
"S" | "SOURCE" => Some(CmdResult::Dispatch(format!(
|
||||||
"ARRAYEDIT_SOURCE:{}",
|
"ARRAYEDIT_SOURCE:{}",
|
||||||
array.value()
|
array.value()
|
||||||
@@ -75,4 +80,6 @@ impl CadCommand for ArrayEditCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["ARRAYEDIT"] });
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
|
names: &["ARRAYEDIT"]
|
||||||
|
});
|
||||||
@@ -1980,41 +1980,37 @@ impl CadCommand for FilletCommand {
|
|||||||
FilletEntity::Line(_) | FilletEntity::Arc(_),
|
FilletEntity::Line(_) | FilletEntity::Arc(_),
|
||||||
EntityType::LwPolyline(moved),
|
EntityType::LwPolyline(moved),
|
||||||
other,
|
other,
|
||||||
) if !same_entity => {
|
) if !same_entity => join_poly_fillet(
|
||||||
join_poly_fillet(
|
poly,
|
||||||
poly,
|
*seg_idx,
|
||||||
*seg_idx,
|
moved,
|
||||||
moved,
|
other,
|
||||||
other,
|
maybe_arc.as_ref(),
|
||||||
maybe_arc.as_ref(),
|
)
|
||||||
)
|
.map(|poly| {
|
||||||
.map(|poly| {
|
vec![
|
||||||
vec![
|
(h1, vec![EntityType::LwPolyline(poly)]),
|
||||||
(h1, vec![EntityType::LwPolyline(poly)]),
|
(handle, Vec::new()),
|
||||||
(handle, Vec::new()),
|
]
|
||||||
]
|
}),
|
||||||
})
|
|
||||||
}
|
|
||||||
(
|
(
|
||||||
FilletEntity::Line(_) | FilletEntity::Arc(_),
|
FilletEntity::Line(_) | FilletEntity::Arc(_),
|
||||||
FilletEntity::LwPoly { poly, seg_idx, .. },
|
FilletEntity::LwPoly { poly, seg_idx, .. },
|
||||||
other,
|
other,
|
||||||
EntityType::LwPolyline(moved),
|
EntityType::LwPolyline(moved),
|
||||||
) if !same_entity => {
|
) if !same_entity => join_poly_fillet(
|
||||||
join_poly_fillet(
|
poly,
|
||||||
poly,
|
*seg_idx,
|
||||||
*seg_idx,
|
moved,
|
||||||
moved,
|
other,
|
||||||
other,
|
maybe_arc.as_ref(),
|
||||||
maybe_arc.as_ref(),
|
)
|
||||||
)
|
.map(|poly| {
|
||||||
.map(|poly| {
|
vec![
|
||||||
vec![
|
(handle, vec![EntityType::LwPolyline(poly)]),
|
||||||
(handle, vec![EntityType::LwPolyline(poly)]),
|
(h1, Vec::new()),
|
||||||
(h1, Vec::new()),
|
]
|
||||||
]
|
}),
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
if let Some(replacements) = joined {
|
if let Some(replacements) = joined {
|
||||||
@@ -2304,8 +2300,7 @@ enum ChamferStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The refusal a no-trim CHAMFER prints for anything but two independent Lines.
|
/// The refusal a no-trim CHAMFER prints for anything but two independent Lines.
|
||||||
const NO_TRIM_CHAMFER_LINES_ONLY: &str =
|
const NO_TRIM_CHAMFER_LINES_ONLY: &str = "No-trim chamfer supports two independent lines only.";
|
||||||
"No-trim chamfer supports two independent lines only.";
|
|
||||||
|
|
||||||
pub struct ChamferCommand {
|
pub struct ChamferCommand {
|
||||||
dist1: f64,
|
dist1: f64,
|
||||||
@@ -2672,9 +2667,7 @@ impl CadCommand for ChamferCommand {
|
|||||||
}
|
}
|
||||||
Some(EntityType::LwPolyline(p)) => {
|
Some(EntityType::LwPolyline(p)) => {
|
||||||
if !self.trim {
|
if !self.trim {
|
||||||
return CmdResult::ReportError(
|
return CmdResult::ReportError(NO_TRIM_CHAMFER_LINES_ONLY.to_string());
|
||||||
NO_TRIM_CHAMFER_LINES_ONLY.to_string(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
self.step = ChamferStep::SecondPoly {
|
self.step = ChamferStep::SecondPoly {
|
||||||
h1: handle,
|
h1: handle,
|
||||||
@@ -2692,9 +2685,7 @@ impl CadCommand for ChamferCommand {
|
|||||||
if !self.trim {
|
if !self.trim {
|
||||||
self.step = ChamferStep::First;
|
self.step = ChamferStep::First;
|
||||||
self.resume_pick = None;
|
self.resume_pick = None;
|
||||||
return CmdResult::ReportError(
|
return CmdResult::ReportError(NO_TRIM_CHAMFER_LINES_ONLY.to_string());
|
||||||
NO_TRIM_CHAMFER_LINES_ONLY.to_string(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let h1 = *h1;
|
let h1 = *h1;
|
||||||
let poly = poly.clone();
|
let poly = poly.clone();
|
||||||
@@ -2927,7 +2918,10 @@ mod tests {
|
|||||||
CmdResult::ReplaceManyContinue(_)
|
CmdResult::ReplaceManyContinue(_)
|
||||||
));
|
));
|
||||||
assert_eq!(keywords(&command), ["U", "P", "R", "T"]);
|
assert_eq!(keywords(&command), ["U", "P", "R", "T"]);
|
||||||
assert!(matches!(command.on_text_input("U"), Some(CmdResult::UndoDocument)));
|
assert!(matches!(
|
||||||
|
command.on_text_input("U"),
|
||||||
|
Some(CmdResult::UndoDocument)
|
||||||
|
));
|
||||||
assert_eq!(keywords(&command), ["P", "R", "T"]);
|
assert_eq!(keywords(&command), ["P", "R", "T"]);
|
||||||
// The host hands the restored document back; the cache follows it.
|
// The host hands the restored document back; the cache follows it.
|
||||||
let mut doc = codec::CadDocument::new();
|
let mut doc = codec::CadDocument::new();
|
||||||
@@ -2941,10 +2935,7 @@ mod tests {
|
|||||||
fn no_trim_fillet_adds_only_an_arc_and_keeps_source_handles() {
|
fn no_trim_fillet_adds_only_an_arc_and_keeps_source_handles() {
|
||||||
let first = Handle::new(1);
|
let first = Handle::new(1);
|
||||||
let second = Handle::new(2);
|
let second = Handle::new(2);
|
||||||
let original = vec![
|
let original = vec![line(0.0, 0.0, 10.0, 0.0, 1), line(0.0, 0.0, 0.0, 10.0, 2)];
|
||||||
line(0.0, 0.0, 10.0, 0.0, 1),
|
|
||||||
line(0.0, 0.0, 0.0, 10.0, 2),
|
|
||||||
];
|
|
||||||
let mut command = FilletCommand::new(1.0, original);
|
let mut command = FilletCommand::new(1.0, original);
|
||||||
command.trim = false;
|
command.trim = false;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@@ -2987,8 +2978,14 @@ mod tests {
|
|||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(source_lines[0], (Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0)));
|
assert_eq!(
|
||||||
assert_eq!(source_lines[1], (Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 10.0, 0.0)));
|
source_lines[0],
|
||||||
|
(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
source_lines[1],
|
||||||
|
(Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 10.0, 0.0))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3003,8 +3000,15 @@ mod tests {
|
|||||||
));
|
));
|
||||||
|
|
||||||
let mut multi = ChamferCommand::new(1.0, lines());
|
let mut multi = ChamferCommand::new(1.0, lines());
|
||||||
assert!(matches!(multi.on_text_input("M"), Some(CmdResult::NeedPoint)));
|
assert!(matches!(
|
||||||
assert_eq!(keywords(&multi), ["D", "N"], "Multiple is not offered twice");
|
multi.on_text_input("M"),
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
keywords(&multi),
|
||||||
|
["D", "N"],
|
||||||
|
"Multiple is not offered twice"
|
||||||
|
);
|
||||||
multi.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0));
|
multi.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0));
|
||||||
match multi.on_entity_pick(Handle::new(2), DVec3::new(0.0, 5.0, 0.0)) {
|
match multi.on_entity_pick(Handle::new(2), DVec3::new(0.0, 5.0, 0.0)) {
|
||||||
CmdResult::ReplaceManyContinue(replacements) => {
|
CmdResult::ReplaceManyContinue(replacements) => {
|
||||||
@@ -3015,17 +3019,17 @@ mod tests {
|
|||||||
_ => panic!("multiple mode should keep the command active"),
|
_ => panic!("multiple mode should keep the command active"),
|
||||||
}
|
}
|
||||||
assert_eq!(keywords(&multi), ["U", "D", "N"]);
|
assert_eq!(keywords(&multi), ["U", "D", "N"]);
|
||||||
assert!(matches!(multi.on_text_input("U"), Some(CmdResult::UndoDocument)));
|
assert!(matches!(
|
||||||
|
multi.on_text_input("U"),
|
||||||
|
Some(CmdResult::UndoDocument)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_trim_chamfer_adds_only_the_cut_line_and_keeps_source_handles() {
|
fn no_trim_chamfer_adds_only_the_cut_line_and_keeps_source_handles() {
|
||||||
let first = Handle::new(1);
|
let first = Handle::new(1);
|
||||||
let second = Handle::new(2);
|
let second = Handle::new(2);
|
||||||
let lines = || vec![
|
let lines = || vec![line(0.0, 0.0, 10.0, 0.0, 1), line(0.0, 0.0, 0.0, 10.0, 2)];
|
||||||
line(0.0, 0.0, 10.0, 0.0, 1),
|
|
||||||
line(0.0, 0.0, 0.0, 10.0, 2),
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut single = ChamferCommand::new(1.0, lines());
|
let mut single = ChamferCommand::new(1.0, lines());
|
||||||
single.dist2 = 1.0;
|
single.dist2 = 1.0;
|
||||||
@@ -3058,7 +3062,10 @@ mod tests {
|
|||||||
|
|
||||||
let mut multi = ChamferCommand::new(1.0, lines());
|
let mut multi = ChamferCommand::new(1.0, lines());
|
||||||
multi.dist2 = 1.0;
|
multi.dist2 = 1.0;
|
||||||
assert!(matches!(multi.on_text_input("M"), Some(CmdResult::NeedPoint)));
|
assert!(matches!(
|
||||||
|
multi.on_text_input("M"),
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
));
|
||||||
multi.trim = false;
|
multi.trim = false;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
multi.on_entity_pick(first, DVec3::new(5.0, 0.0, 0.0)),
|
multi.on_entity_pick(first, DVec3::new(5.0, 0.0, 0.0)),
|
||||||
@@ -3082,12 +3089,18 @@ mod tests {
|
|||||||
fn no_trim_chamfer_refuses_polyline_corners() {
|
fn no_trim_chamfer_refuses_polyline_corners() {
|
||||||
for token in ["N", "NOTRIM", "NO TRIM", "TRIM=FALSE"] {
|
for token in ["N", "NOTRIM", "NO TRIM", "TRIM=FALSE"] {
|
||||||
let mut command = ChamferCommand::new(1.0, Vec::new());
|
let mut command = ChamferCommand::new(1.0, Vec::new());
|
||||||
assert!(matches!(command.on_text_input(token), Some(CmdResult::NeedPoint)));
|
assert!(matches!(
|
||||||
|
command.on_text_input(token),
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
));
|
||||||
assert!(!command.trim, "{token}");
|
assert!(!command.trim, "{token}");
|
||||||
}
|
}
|
||||||
for token in ["T", "TRIM", "TRIM=TRUE"] {
|
for token in ["T", "TRIM", "TRIM=TRUE"] {
|
||||||
let mut command = ChamferCommand::new(1.0, Vec::new());
|
let mut command = ChamferCommand::new(1.0, Vec::new());
|
||||||
assert!(matches!(command.on_text_input(token), Some(CmdResult::NeedPoint)));
|
assert!(matches!(
|
||||||
|
command.on_text_input(token),
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
));
|
||||||
assert!(command.trim, "{token}");
|
assert!(command.trim, "{token}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -473,7 +473,10 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>,
|
|||||||
let (Some(pa), Some(pb)) = (&ends[a], &ends[b]) else {
|
let (Some(pa), Some(pb)) = (&ends[a], &ends[b]) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if pa.iter().any(|p| pb.iter().any(|q| p.distance(*q) <= JOIN_EPS)) {
|
if pa
|
||||||
|
.iter()
|
||||||
|
.any(|p| pb.iter().any(|q| p.distance(*q) <= JOIN_EPS))
|
||||||
|
{
|
||||||
let (ra, rb) = (root(&mut group, a), root(&mut group, b));
|
let (ra, rb) = (root(&mut group, a), root(&mut group, b));
|
||||||
group[ra] = rb;
|
group[ra] = rb;
|
||||||
}
|
}
|
||||||
@@ -483,7 +486,10 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>,
|
|||||||
Default::default();
|
Default::default();
|
||||||
for (index, entity) in entities.iter().enumerate() {
|
for (index, entity) in entities.iter().enumerate() {
|
||||||
if ends[index].is_some() {
|
if ends[index].is_some() {
|
||||||
chains.entry(root(&mut group, index)).or_default().push(*entity);
|
chains
|
||||||
|
.entry(root(&mut group, index))
|
||||||
|
.or_default()
|
||||||
|
.push(*entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let (mut removed, mut merged) = (Vec::new(), Vec::new());
|
let (mut removed, mut merged) = (Vec::new(), Vec::new());
|
||||||
@@ -797,8 +803,10 @@ mod join_tests {
|
|||||||
line(o, o + size, o, o, 0.0),
|
line(o, o + size, o, o, 0.0),
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
let lines: Vec<EntityType> =
|
let lines: Vec<EntityType> = square(0.0, 10.0)
|
||||||
square(0.0, 10.0).into_iter().chain(square(2.0, 6.0)).collect();
|
.into_iter()
|
||||||
|
.chain(square(2.0, 6.0))
|
||||||
|
.collect();
|
||||||
let lone = line(50.0, 0.0, 60.0, 0.0, 0.0);
|
let lone = line(50.0, 0.0, 60.0, 0.0, 0.0);
|
||||||
let mut pairs: Vec<(Handle, &EntityType)> = lines
|
let mut pairs: Vec<(Handle, &EntityType)> = lines
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -302,7 +302,10 @@ pub fn nesting_chain(
|
|||||||
return chain;
|
return chain;
|
||||||
};
|
};
|
||||||
for _ in 0..16 {
|
for _ in 0..16 {
|
||||||
let level = chain.last().expect("chain starts with the top level").clone();
|
let level = chain
|
||||||
|
.last()
|
||||||
|
.expect("chain starts with the top level")
|
||||||
|
.clone();
|
||||||
let nearest = level_hits(document, &level)
|
let nearest = level_hits(document, &level)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(h, paths)| paths_distance(&paths, point).map(|d| (h, d)))
|
.filter_map(|(h, paths)| paths_distance(&paths, point).map(|d| (h, d)))
|
||||||
@@ -760,7 +763,6 @@ pub fn inverse_placement(t: &codec::types::Transform) -> Option<codec::types::Tr
|
|||||||
Some(codec::types::Transform::from_matrix(out))
|
Some(codec::types::Transform::from_matrix(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Apply the session's forward transform to a definition-local entity so it
|
/// Apply the session's forward transform to a definition-local entity so it
|
||||||
/// appears at its correct world-space position.
|
/// appears at its correct world-space position.
|
||||||
pub fn apply_insert_transform(entity: &mut EntityType, session: &RefEditSession) {
|
pub fn apply_insert_transform(entity: &mut EntityType, session: &RefEditSession) {
|
||||||
|
|||||||
@@ -4950,7 +4950,11 @@ mod tests {
|
|||||||
let geos = build_geos(&all);
|
let geos = build_geos(&all);
|
||||||
let curve_line = entity_curve_xy(&EntityType::Line(l1.clone())).unwrap();
|
let curve_line = entity_curve_xy(&EntityType::Line(l1.clone())).unwrap();
|
||||||
let ts_line = cut_params(&curve_line, Handle::new(1), &geos);
|
let ts_line = cut_params(&curve_line, Handle::new(1), &geos);
|
||||||
assert!(!ts_line.is_empty(), "line must have cut point where tangent arc touches it: ts={:?}", ts_line);
|
assert!(
|
||||||
|
!ts_line.is_empty(),
|
||||||
|
"line must have cut point where tangent arc touches it: ts={:?}",
|
||||||
|
ts_line
|
||||||
|
);
|
||||||
|
|
||||||
let curve_arc = entity_curve_xy(&EntityType::Arc(a.clone())).unwrap();
|
let curve_arc = entity_curve_xy(&EntityType::Arc(a.clone())).unwrap();
|
||||||
let ts_arc = cut_params(&curve_arc, Handle::new(2), &geos);
|
let ts_arc = cut_params(&curve_arc, Handle::new(2), &geos);
|
||||||
@@ -4959,9 +4963,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_trim_arc_tangent_to_two_lines() {
|
fn test_trim_arc_tangent_to_two_lines() {
|
||||||
use codec::entities::{Arc as CadArc, Line as LineEnt};
|
|
||||||
use crate::command::{TangentObject, WorkingPlane};
|
use crate::command::{TangentObject, WorkingPlane};
|
||||||
use crate::modules::draw::draw::arc::{solve_arc_3_inputs, ArcPick};
|
use crate::modules::draw::draw::arc::{solve_arc_3_inputs, ArcPick};
|
||||||
|
use codec::entities::{Arc as CadArc, Line as LineEnt};
|
||||||
|
|
||||||
let mut l1 = LineEnt::new();
|
let mut l1 = LineEnt::new();
|
||||||
l1.common.handle = Handle::new(1);
|
l1.common.handle = Handle::new(1);
|
||||||
@@ -4985,9 +4989,15 @@ mod tests {
|
|||||||
// Middle point: (1 - sqrt(0.5), 1 - sqrt(0.5)) -> radius ~ 1
|
// Middle point: (1 - sqrt(0.5), 1 - sqrt(0.5)) -> radius ~ 1
|
||||||
let mid = DVec3::new(1.0 - (0.5_f64).sqrt(), 1.0 - (0.5_f64).sqrt(), 0.0);
|
let mid = DVec3::new(1.0 - (0.5_f64).sqrt(), 1.0 - (0.5_f64).sqrt(), 0.0);
|
||||||
let sol = solve_arc_3_inputs(
|
let sol = solve_arc_3_inputs(
|
||||||
ArcPick::Tangent { target: t1, hit: DVec3::new(2.0, 0.0, 0.0) },
|
ArcPick::Tangent {
|
||||||
|
target: t1,
|
||||||
|
hit: DVec3::new(2.0, 0.0, 0.0),
|
||||||
|
},
|
||||||
ArcPick::Point(mid),
|
ArcPick::Point(mid),
|
||||||
ArcPick::Tangent { target: t2, hit: DVec3::new(0.0, 2.0, 0.0) },
|
ArcPick::Tangent {
|
||||||
|
target: t2,
|
||||||
|
hit: DVec3::new(0.0, 2.0, 0.0),
|
||||||
|
},
|
||||||
WorkingPlane::default(),
|
WorkingPlane::default(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -5038,9 +5048,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_trim_arc_tangent_angled_lines() {
|
fn test_trim_arc_tangent_angled_lines() {
|
||||||
use codec::entities::{Arc as CadArc, Line as LineEnt};
|
|
||||||
use crate::command::{TangentObject, WorkingPlane};
|
use crate::command::{TangentObject, WorkingPlane};
|
||||||
use crate::modules::draw::draw::arc::{solve_arc_3_inputs, ArcPick};
|
use crate::modules::draw::draw::arc::{solve_arc_3_inputs, ArcPick};
|
||||||
|
use codec::entities::{Arc as CadArc, Line as LineEnt};
|
||||||
|
|
||||||
// Two lines meeting at (0, 0): one along (cos(20 deg), sin(20 deg)), other along (cos(80 deg), sin(80 deg))
|
// Two lines meeting at (0, 0): one along (cos(20 deg), sin(20 deg)), other along (cos(80 deg), sin(80 deg))
|
||||||
let a1 = 20.0_f64.to_radians();
|
let a1 = 20.0_f64.to_radians();
|
||||||
@@ -5069,12 +5079,22 @@ mod tests {
|
|||||||
|
|
||||||
// Midpoint between points at distance 20 on the bisector
|
// Midpoint between points at distance 20 on the bisector
|
||||||
let bisector_angle = (a1 + a2) * 0.5;
|
let bisector_angle = (a1 + a2) * 0.5;
|
||||||
let mid = DVec3::new(15.0 * bisector_angle.cos(), 15.0 * bisector_angle.sin(), 0.0);
|
let mid = DVec3::new(
|
||||||
|
15.0 * bisector_angle.cos(),
|
||||||
|
15.0 * bisector_angle.sin(),
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
|
||||||
let sol = solve_arc_3_inputs(
|
let sol = solve_arc_3_inputs(
|
||||||
ArcPick::Tangent { target: t1, hit: DVec3::new(20.0 * dir1.x, 20.0 * dir1.y, 0.0) },
|
ArcPick::Tangent {
|
||||||
|
target: t1,
|
||||||
|
hit: DVec3::new(20.0 * dir1.x, 20.0 * dir1.y, 0.0),
|
||||||
|
},
|
||||||
ArcPick::Point(mid),
|
ArcPick::Point(mid),
|
||||||
ArcPick::Tangent { target: t2, hit: DVec3::new(20.0 * dir2.x, 20.0 * dir2.y, 0.0) },
|
ArcPick::Tangent {
|
||||||
|
target: t2,
|
||||||
|
hit: DVec3::new(20.0 * dir2.x, 20.0 * dir2.y, 0.0),
|
||||||
|
},
|
||||||
WorkingPlane::default(),
|
WorkingPlane::default(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -5102,8 +5122,16 @@ mod tests {
|
|||||||
println!("angled ts_l2: {:?}", ts_l2);
|
println!("angled ts_l2: {:?}", ts_l2);
|
||||||
|
|
||||||
// We expect l1 to have at least 2 cuts: the intersection with l2 at (0,0) and the tangent point with arc!
|
// We expect l1 to have at least 2 cuts: the intersection with l2 at (0,0) and the tangent point with arc!
|
||||||
assert!(ts_l1.len() >= 2, "Line 1 must have intersection and tangent cut: got {:?}", ts_l1);
|
assert!(
|
||||||
assert!(ts_l2.len() >= 2, "Line 2 must have intersection and tangent cut: got {:?}", ts_l2);
|
ts_l1.len() >= 2,
|
||||||
|
"Line 1 must have intersection and tangent cut: got {:?}",
|
||||||
|
ts_l1
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ts_l2.len() >= 2,
|
||||||
|
"Line 2 must have intersection and tangent cut: got {:?}",
|
||||||
|
ts_l2
|
||||||
|
);
|
||||||
|
|
||||||
// Trimming l1 arm past the tangent point
|
// Trimming l1 arm past the tangent point
|
||||||
let click_l1 = 40.0 * dir1;
|
let click_l1 = 40.0 * dir1;
|
||||||
@@ -5115,7 +5143,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_kernel_intersect_tangency() {
|
fn test_kernel_intersect_tangency() {
|
||||||
use std::f64::consts::FRAC_PI_2;
|
use std::f64::consts::FRAC_PI_2;
|
||||||
let line = Curve::Line(KernelLine { start: [-10.0, 0.0], end: [10.0, 0.0] });
|
let line = Curve::Line(KernelLine {
|
||||||
|
start: [-10.0, 0.0],
|
||||||
|
end: [10.0, 0.0],
|
||||||
|
});
|
||||||
|
|
||||||
// Case 1: Exact tangency at interior of arc
|
// Case 1: Exact tangency at interior of arc
|
||||||
let arc_exact = Curve::Arc(KernelArc {
|
let arc_exact = Curve::Arc(KernelArc {
|
||||||
@@ -5178,14 +5209,18 @@ mod tests {
|
|||||||
println!("hits_end (-PI/2): {:?}", hits_end);
|
println!("hits_end (-PI/2): {:?}", hits_end);
|
||||||
|
|
||||||
// Case 7: Large coordinates (e.g. 5000, 5005)
|
// Case 7: Large coordinates (e.g. 5000, 5005)
|
||||||
let line_large = Curve::Line(KernelLine { start: [4990.0, 5000.0], end: [5010.0, 5000.0] });
|
let line_large = Curve::Line(KernelLine {
|
||||||
|
start: [4990.0, 5000.0],
|
||||||
|
end: [5010.0, 5000.0],
|
||||||
|
});
|
||||||
let arc_large = Curve::Arc(KernelArc {
|
let arc_large = Curve::Arc(KernelArc {
|
||||||
centre: [5000.0, 5005.0],
|
centre: [5000.0, 5005.0],
|
||||||
radius: 5.0,
|
radius: 5.0,
|
||||||
start_angle: -FRAC_PI_2,
|
start_angle: -FRAC_PI_2,
|
||||||
end_angle: 0.0,
|
end_angle: 0.0,
|
||||||
});
|
});
|
||||||
let hits_large = kernel_intersect(&line_large, &arc_large, KernelTolerance::new(CUT_TOLERANCE));
|
let hits_large =
|
||||||
|
kernel_intersect(&line_large, &arc_large, KernelTolerance::new(CUT_TOLERANCE));
|
||||||
println!("hits_large: {:?}", hits_large);
|
println!("hits_large: {:?}", hits_large);
|
||||||
|
|
||||||
// Test with cut_params:
|
// Test with cut_params:
|
||||||
@@ -5203,9 +5238,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_arc_tangent_line_precision_sweep() {
|
fn test_arc_tangent_line_precision_sweep() {
|
||||||
use codec::entities::{Arc as CadArc, Line as LineEnt};
|
|
||||||
use crate::command::{TangentObject, WorkingPlane};
|
use crate::command::{TangentObject, WorkingPlane};
|
||||||
use crate::modules::draw::draw::arc::{solve_arc_3_inputs, ArcPick};
|
use crate::modules::draw::draw::arc::{solve_arc_3_inputs, ArcPick};
|
||||||
|
use codec::entities::{Arc as CadArc, Line as LineEnt};
|
||||||
|
|
||||||
let mut max_dist = 0.0_f64;
|
let mut max_dist = 0.0_f64;
|
||||||
let mut failed_cuts = 0;
|
let mut failed_cuts = 0;
|
||||||
@@ -5240,9 +5275,15 @@ mod tests {
|
|||||||
let mid = bisector * 30.0;
|
let mid = bisector * 30.0;
|
||||||
|
|
||||||
let sol = solve_arc_3_inputs(
|
let sol = solve_arc_3_inputs(
|
||||||
ArcPick::Tangent { target: t1, hit: DVec3::new(50.0, 0.0, 0.0) },
|
ArcPick::Tangent {
|
||||||
|
target: t1,
|
||||||
|
hit: DVec3::new(50.0, 0.0, 0.0),
|
||||||
|
},
|
||||||
ArcPick::Point(mid),
|
ArcPick::Point(mid),
|
||||||
ArcPick::Tangent { target: t2, hit: dir2 * 50.0 },
|
ArcPick::Tangent {
|
||||||
|
target: t2,
|
||||||
|
hit: dir2 * 50.0,
|
||||||
|
},
|
||||||
WorkingPlane::default(),
|
WorkingPlane::default(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -5269,13 +5310,18 @@ mod tests {
|
|||||||
|
|
||||||
if ts_l1.len() < 2 || ts_l2.len() < 2 {
|
if ts_l1.len() < 2 || ts_l2.len() < 2 {
|
||||||
failed_cuts += 1;
|
failed_cuts += 1;
|
||||||
println!("FAILED at angle {angle_deg} deg: ts_l1={:?}, ts_l2={:?}", ts_l1, ts_l2);
|
println!(
|
||||||
|
"FAILED at angle {angle_deg} deg: ts_l1={:?}, ts_l2={:?}",
|
||||||
|
ts_l1, ts_l2
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check distance of arc endpoints to line
|
// Check distance of arc endpoints to line
|
||||||
let ep1 = [center.x + radius * sa.cos(), center.y + radius * sa.sin()];
|
let ep1 = [center.x + radius * sa.cos(), center.y + radius * sa.sin()];
|
||||||
let d1 = (ep1[1] - 0.0).abs(); // distance to l1 (y=0)
|
let d1 = (ep1[1] - 0.0).abs(); // distance to l1 (y=0)
|
||||||
if d1 > max_dist { max_dist = d1; }
|
if d1 > max_dist {
|
||||||
|
max_dist = d1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("Sweep results: failed_cuts={failed_cuts}, max_dist={max_dist:e}");
|
println!("Sweep results: failed_cuts={failed_cuts}, max_dist={max_dist:e}");
|
||||||
|
|||||||
@@ -206,7 +206,10 @@ impl AdjustCommand {
|
|||||||
fn apply(&self, prop: Prop, value: u8) -> CmdResult {
|
fn apply(&self, prop: Prop, value: u8) -> CmdResult {
|
||||||
if let Subject::Underlay { .. } = self.subject {
|
if let Subject::Underlay { .. } = self.subject {
|
||||||
if let Ok(mut last) = LAST_UNDERLAY.lock() {
|
if let Ok(mut last) = LAST_UNDERLAY.lock() {
|
||||||
last.0 = UNDERLAY_OPTIONS.iter().position(|p| *p == prop).unwrap_or(0);
|
last.0 = UNDERLAY_OPTIONS
|
||||||
|
.iter()
|
||||||
|
.position(|p| *p == prop)
|
||||||
|
.unwrap_or(0);
|
||||||
match prop {
|
match prop {
|
||||||
Prop::Fade => last.1 = value,
|
Prop::Fade => last.1 = value,
|
||||||
Prop::Contrast => last.2 = value,
|
Prop::Contrast => last.2 = value,
|
||||||
@@ -341,8 +344,12 @@ impl CadCommand for AdjustCommand {
|
|||||||
t!("Value must be an integer between 0 and 100.").into_owned(),
|
t!("Value must be an integer between 0 and 100.").into_owned(),
|
||||||
)),
|
)),
|
||||||
(None, Subject::Underlay { .. }) => Some(CmdResult::ReportError(
|
(None, Subject::Underlay { .. }) => Some(CmdResult::ReportError(
|
||||||
crate::tf!("Requires an integer between {min} and {max}.", min = 0, max = 100)
|
crate::tf!(
|
||||||
.into_owned(),
|
"Requires an integer between {min} and {max}.",
|
||||||
|
min = 0,
|
||||||
|
max = 100
|
||||||
|
)
|
||||||
|
.into_owned(),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,5 +366,11 @@ impl CadCommand for AdjustCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inventory::submit!(crate::command::CommandRegistration {
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
names: &["IMAGEADJUST", "-IMAGEADJUST", "PDFADJUST", "DWFADJUST", "DGNADJUST"]
|
names: &[
|
||||||
|
"IMAGEADJUST",
|
||||||
|
"-IMAGEADJUST",
|
||||||
|
"PDFADJUST",
|
||||||
|
"DWFADJUST",
|
||||||
|
"DGNADJUST"
|
||||||
|
]
|
||||||
});
|
});
|
||||||
@@ -292,7 +292,9 @@ impl CadCommand for MvsetupCommand {
|
|||||||
let count = if text.is_empty() {
|
let count = if text.is_empty() {
|
||||||
2
|
2
|
||||||
} else {
|
} else {
|
||||||
text.parse::<u32>().ok().filter(|count| (1..=100).contains(count))?
|
text.parse::<u32>()
|
||||||
|
.ok()
|
||||||
|
.filter(|count| (1..=100).contains(count))?
|
||||||
};
|
};
|
||||||
if self.step == Step::Columns {
|
if self.step == Step::Columns {
|
||||||
self.columns = count;
|
self.columns = count;
|
||||||
@@ -325,9 +327,7 @@ impl CadCommand for MvsetupCommand {
|
|||||||
factor: self.paper_units / model_units,
|
factor: self.paper_units / model_units,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Step::FirstCorner | Step::SecondCorner | Step::Delete | Step::Viewports => {
|
Step::FirstCorner | Step::SecondCorner | Step::Delete | Step::Viewports => return None,
|
||||||
return None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some(CmdResult::NeedPoint)
|
Some(CmdResult::NeedPoint)
|
||||||
}
|
}
|
||||||
@@ -357,7 +357,10 @@ impl CadCommand for MvsetupCommand {
|
|||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
CmdResult::ReplaceManyContinue(
|
CmdResult::ReplaceManyContinue(
|
||||||
erased.into_iter().map(|handle| (handle, Vec::new())).collect(),
|
erased
|
||||||
|
.into_iter()
|
||||||
|
.map(|handle| (handle, Vec::new()))
|
||||||
|
.collect(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Step::Viewports if self.picked.is_empty() => CmdResult::Cancel,
|
Step::Viewports if self.picked.is_empty() => CmdResult::Cancel,
|
||||||
@@ -371,4 +374,6 @@ impl CadCommand for MvsetupCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["MVSETUP"] });
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
|
names: &["MVSETUP"]
|
||||||
|
});
|
||||||
@@ -74,7 +74,12 @@ impl CadCommand for CompareCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
match text.trim().trim_start_matches('_').to_ascii_uppercase().as_str() {
|
match text
|
||||||
|
.trim()
|
||||||
|
.trim_start_matches('_')
|
||||||
|
.to_ascii_uppercase()
|
||||||
|
.as_str()
|
||||||
|
{
|
||||||
"" | "N" | "NEXT" => Some(self.show(true)),
|
"" | "N" | "NEXT" => Some(self.show(true)),
|
||||||
"P" | "PREVIOUS" => Some(self.show(false)),
|
"P" | "PREVIOUS" => Some(self.show(false)),
|
||||||
"X" | "EXIT" => Some(CmdResult::Cancel),
|
"X" | "EXIT" => Some(CmdResult::Cancel),
|
||||||
@@ -101,4 +106,6 @@ impl CadCommand for CompareCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["COMPARE"] });
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
|
names: &["COMPARE"]
|
||||||
|
});
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
mod cascade;
|
mod cascade;
|
||||||
mod count_palette;
|
mod count_palette;
|
||||||
|
pub mod dview;
|
||||||
mod file_tabs;
|
mod file_tabs;
|
||||||
mod graphic_attributes_palette;
|
mod graphic_attributes_palette;
|
||||||
mod layout_tabs;
|
mod layout_tabs;
|
||||||
pub mod dview;
|
|
||||||
pub mod limits;
|
pub mod limits;
|
||||||
mod orbit;
|
mod orbit;
|
||||||
mod ortho;
|
mod ortho;
|
||||||
@@ -19,8 +19,8 @@ mod tile_horiz;
|
|||||||
mod tile_vert;
|
mod tile_vert;
|
||||||
mod tool_palettes;
|
mod tool_palettes;
|
||||||
pub mod ucs_cmd;
|
pub mod ucs_cmd;
|
||||||
pub mod view_cmd;
|
|
||||||
mod ucs_icon;
|
mod ucs_icon;
|
||||||
|
pub mod view_cmd;
|
||||||
mod view_front;
|
mod view_front;
|
||||||
mod view_iso;
|
mod view_iso;
|
||||||
mod view_right;
|
mod view_right;
|
||||||
|
|||||||
+124
-33
@@ -33,8 +33,9 @@ pub fn set_ucs_view(on: bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The standard orthographic and isometric view presets.
|
/// The standard orthographic and isometric view presets.
|
||||||
pub const PRESETS: [&str; 10] =
|
pub const PRESETS: [&str; 10] = [
|
||||||
["TOP", "BOTTOM", "FRONT", "BACK", "LEFT", "RIGHT", "SWISO", "SEISO", "NEISO", "NWISO"];
|
"TOP", "BOTTOM", "FRONT", "BACK", "LEFT", "RIGHT", "SWISO", "SEISO", "NEISO", "NWISO",
|
||||||
|
];
|
||||||
|
|
||||||
/// The orthographic UCS an orthographic preset sets while UCSORTHO is on:
|
/// The orthographic UCS an orthographic preset sets while UCSORTHO is on:
|
||||||
/// its name, X and Y axes and ORTHOGRAPHIC type, as the reference sets them.
|
/// its name, X and Y axes and ORTHOGRAPHIC type, as the reference sets them.
|
||||||
@@ -152,17 +153,27 @@ fn keyword(input: &str, options: &[&'static str]) -> Option<&'static str> {
|
|||||||
.find(|word| word.chars().any(|c| c.is_ascii_uppercase()))
|
.find(|word| word.chars().any(|c| c.is_ascii_uppercase()))
|
||||||
.unwrap_or(option);
|
.unwrap_or(option);
|
||||||
let capitals: String = word.chars().filter(char::is_ascii_uppercase).collect();
|
let capitals: String = word.chars().filter(char::is_ascii_uppercase).collect();
|
||||||
let reach = word.rfind(|c: char| c.is_ascii_uppercase()).map_or(1, |at| at + 1);
|
let reach = word
|
||||||
|
.rfind(|c: char| c.is_ascii_uppercase())
|
||||||
|
.map_or(1, |at| at + 1);
|
||||||
input == capitals || (word.to_ascii_uppercase().starts_with(&input) && input.len() >= reach)
|
input == capitals || (word.to_ascii_uppercase().starts_with(&input) && input.len() >= reach)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The names in `names` that one of the comma-separated wildcard patterns matches.
|
/// The names in `names` that one of the comma-separated wildcard patterns matches.
|
||||||
pub fn matching<'a>(names: impl IntoIterator<Item = &'a str>, patterns: &str) -> Vec<&'a str> {
|
pub fn matching<'a>(names: impl IntoIterator<Item = &'a str>, patterns: &str) -> Vec<&'a str> {
|
||||||
let patterns: Vec<&str> = patterns.split(',').map(str::trim).filter(|p| !p.is_empty()).collect();
|
let patterns: Vec<&str> = patterns
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|p| !p.is_empty())
|
||||||
|
.collect();
|
||||||
names
|
names
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|name| patterns.iter().any(|pattern| crate::io::xref_model::wildcard_match(name, pattern)))
|
.filter(|name| {
|
||||||
|
patterns
|
||||||
|
.iter()
|
||||||
|
.any(|pattern| crate::io::xref_model::wildcard_match(name, pattern))
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,23 +187,36 @@ pub fn view_listing(views: &[(String, bool)]) -> String {
|
|||||||
}
|
}
|
||||||
out.push_str("\nView name Space");
|
out.push_str("\nView name Space");
|
||||||
for (name, paper) in views {
|
for (name, paper) in views {
|
||||||
out.push_str(&format!("\n{:<35}{}", format!("\"{name}\""), if *paper { "P" } else { "M" }));
|
out.push_str(&format!(
|
||||||
|
"\n{:<35}{}",
|
||||||
|
format!("\"{name}\""),
|
||||||
|
if *paper { "P" } else { "M" }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DashViewCommand {
|
impl DashViewCommand {
|
||||||
pub fn new(names: ViewNames) -> Self {
|
pub fn new(names: ViewNames) -> Self {
|
||||||
Self { names, step: Step::Main }
|
Self {
|
||||||
|
names,
|
||||||
|
step: Step::Main,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The command back at its settings prompt, after a setting was applied.
|
/// The command back at its settings prompt, after a setting was applied.
|
||||||
pub fn settings(names: ViewNames) -> Self {
|
pub fn settings(names: ViewNames) -> Self {
|
||||||
Self { names, step: Step::Settings }
|
Self {
|
||||||
|
names,
|
||||||
|
step: Step::Settings,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, name: &str) -> Option<&ViewInfo> {
|
fn view(&self, name: &str) -> Option<&ViewInfo> {
|
||||||
self.names.views.iter().find(|view| view.name.eq_ignore_ascii_case(name))
|
self.names
|
||||||
|
.views
|
||||||
|
.iter()
|
||||||
|
.find(|view| view.name.eq_ignore_ascii_case(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn end(message: &str) -> CmdResult {
|
fn end(message: &str) -> CmdResult {
|
||||||
@@ -200,19 +224,35 @@ impl DashViewCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn list_views(&self, pattern: &str) -> String {
|
fn list_views(&self, pattern: &str) -> String {
|
||||||
let pattern = if pattern.trim().is_empty() { "*" } else { pattern };
|
let pattern = if pattern.trim().is_empty() {
|
||||||
let names: Vec<&str> = self.names.views.iter().map(|view| view.name.as_str()).collect();
|
"*"
|
||||||
let found: Vec<(String, bool)> =
|
} else {
|
||||||
matching(names, pattern).into_iter().map(|name| (name.to_string(), false)).collect();
|
pattern
|
||||||
|
};
|
||||||
|
let names: Vec<&str> = self
|
||||||
|
.names
|
||||||
|
.views
|
||||||
|
.iter()
|
||||||
|
.map(|view| view.name.as_str())
|
||||||
|
.collect();
|
||||||
|
let found: Vec<(String, bool)> = matching(names, pattern)
|
||||||
|
.into_iter()
|
||||||
|
.map(|name| (name.to_string(), false))
|
||||||
|
.collect();
|
||||||
view_listing(&found)
|
view_listing(&found)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn value_prompt(&self, setting: Setting, view: &str) -> String {
|
fn value_prompt(&self, setting: Setting, view: &str) -> String {
|
||||||
let info = self.view(view).cloned().unwrap_or_default();
|
let info = self.view(view).cloned().unwrap_or_default();
|
||||||
match setting {
|
match setting {
|
||||||
Setting::Background => "Specify background type [Color/Gradient/Image/None]<None>:".into(),
|
Setting::Background => {
|
||||||
|
"Specify background type [Color/Gradient/Image/None]<None>:".into()
|
||||||
|
}
|
||||||
Setting::Categorize => {
|
Setting::Categorize => {
|
||||||
format!("Enter category name or * for none, or [?]: <\"{}\">", info.category)
|
format!(
|
||||||
|
"Enter category name or * for none, or [?]: <\"{}\">",
|
||||||
|
info.category
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Setting::LayerSnapshot => "Enter an option [Save/Delete] <Cancel>:".into(),
|
Setting::LayerSnapshot => "Enter an option [Save/Delete] <Cancel>:".into(),
|
||||||
Setting::LiveSection => format!(
|
Setting::LiveSection => format!(
|
||||||
@@ -229,7 +269,10 @@ impl DashViewCommand {
|
|||||||
/// Hand a setting to the inline handler, which applies it and starts the
|
/// Hand a setting to the inline handler, which applies it and starts the
|
||||||
/// settings prompt again.
|
/// settings prompt again.
|
||||||
fn apply(setting: Setting, view: &str, value: &str) -> CmdResult {
|
fn apply(setting: Setting, view: &str, value: &str) -> CmdResult {
|
||||||
CmdResult::Dispatch(format!("-VIEW >SET {}\u{1f}{view}\u{1f}{value}", setting.verb()))
|
CmdResult::Dispatch(format!(
|
||||||
|
"-VIEW >SET {}\u{1f}{view}\u{1f}{value}",
|
||||||
|
setting.verb()
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_setting_value(&mut self, setting: Setting, view: String, text: &str) -> CmdResult {
|
fn on_setting_value(&mut self, setting: Setting, view: String, text: &str) -> CmdResult {
|
||||||
@@ -302,7 +345,11 @@ impl DashViewCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn value_listing(&self, setting: Setting, pattern: &str) -> String {
|
fn value_listing(&self, setting: Setting, pattern: &str) -> String {
|
||||||
let pattern = if pattern.trim().is_empty() { "*" } else { pattern };
|
let pattern = if pattern.trim().is_empty() {
|
||||||
|
"*"
|
||||||
|
} else {
|
||||||
|
pattern
|
||||||
|
};
|
||||||
match setting {
|
match setting {
|
||||||
Setting::Categorize => {
|
Setting::Categorize => {
|
||||||
let mut categories: Vec<&str> = self
|
let mut categories: Vec<&str> = self
|
||||||
@@ -401,7 +448,18 @@ impl CadCommand for DashViewCommand {
|
|||||||
if let Some(preset) = PRESETS.iter().find(|preset| **preset == word) {
|
if let Some(preset) = PRESETS.iter().find(|preset| **preset == word) {
|
||||||
return Some(CmdResult::Dispatch(format!("-VIEW >PRESET {preset}")));
|
return Some(CmdResult::Dispatch(format!("-VIEW >PRESET {preset}")));
|
||||||
}
|
}
|
||||||
match keyword(t, &["?", "Delete", "Orthographic", "Restore", "Save", "sEttings", "Window"]) {
|
match keyword(
|
||||||
|
t,
|
||||||
|
&[
|
||||||
|
"?",
|
||||||
|
"Delete",
|
||||||
|
"Orthographic",
|
||||||
|
"Restore",
|
||||||
|
"Save",
|
||||||
|
"sEttings",
|
||||||
|
"Window",
|
||||||
|
],
|
||||||
|
) {
|
||||||
Some("?") => self.step = Step::List,
|
Some("?") => self.step = Step::List,
|
||||||
Some("Delete") => self.step = Step::Delete,
|
Some("Delete") => self.step = Step::Delete,
|
||||||
Some("Orthographic") => self.step = Step::Orthographic,
|
Some("Orthographic") => self.step = Step::Orthographic,
|
||||||
@@ -415,7 +473,9 @@ impl CadCommand for DashViewCommand {
|
|||||||
}
|
}
|
||||||
Step::Orthographic => {
|
Step::Orthographic => {
|
||||||
match keyword(t, &["Top", "Bottom", "Front", "BAck", "Left", "Right"]) {
|
match keyword(t, &["Top", "Bottom", "Front", "BAck", "Left", "Right"]) {
|
||||||
Some(face) => CmdResult::Dispatch(format!("-VIEW >PRESET {}", face.to_ascii_uppercase())),
|
Some(face) => {
|
||||||
|
CmdResult::Dispatch(format!("-VIEW >PRESET {}", face.to_ascii_uppercase()))
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
self.step = Step::Orthographic;
|
self.step = Step::Orthographic;
|
||||||
CmdResult::ReportError("Invalid option keyword.".into())
|
CmdResult::ReportError("Invalid option keyword.".into())
|
||||||
@@ -424,7 +484,12 @@ impl CadCommand for DashViewCommand {
|
|||||||
}
|
}
|
||||||
Step::List => CmdResult::Measurement(self.list_views(t)),
|
Step::List => CmdResult::Measurement(self.list_views(t)),
|
||||||
Step::Delete => {
|
Step::Delete => {
|
||||||
let names: Vec<&str> = self.names.views.iter().map(|view| view.name.as_str()).collect();
|
let names: Vec<&str> = self
|
||||||
|
.names
|
||||||
|
.views
|
||||||
|
.iter()
|
||||||
|
.map(|view| view.name.as_str())
|
||||||
|
.collect();
|
||||||
if matching(names, t).is_empty() {
|
if matching(names, t).is_empty() {
|
||||||
Self::end("No matching view names found.")
|
Self::end("No matching view names found.")
|
||||||
} else {
|
} else {
|
||||||
@@ -450,11 +515,20 @@ impl CadCommand for DashViewCommand {
|
|||||||
self.step = Step::WindowFirst(t.to_string());
|
self.step = Step::WindowFirst(t.to_string());
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
Step::WindowFirst(_) | Step::WindowSecond(..) => Self::end("Invalid window specification."),
|
Step::WindowFirst(_) | Step::WindowSecond(..) => {
|
||||||
|
Self::end("Invalid window specification.")
|
||||||
|
}
|
||||||
Step::Settings => {
|
Step::Settings => {
|
||||||
match keyword(
|
match keyword(
|
||||||
t,
|
t,
|
||||||
&["Background", "Categorize", "Layer snapshot", "live Section", "Ucs", "Visual style"],
|
&[
|
||||||
|
"Background",
|
||||||
|
"Categorize",
|
||||||
|
"Layer snapshot",
|
||||||
|
"live Section",
|
||||||
|
"Ucs",
|
||||||
|
"Visual style",
|
||||||
|
],
|
||||||
) {
|
) {
|
||||||
Some("Background") => self.step = Step::SettingView(Setting::Background),
|
Some("Background") => self.step = Step::SettingView(Setting::Background),
|
||||||
Some("Categorize") => self.step = Step::SettingView(Setting::Categorize),
|
Some("Categorize") => self.step = Step::SettingView(Setting::Categorize),
|
||||||
@@ -496,7 +570,9 @@ impl CadCommand for DashViewCommand {
|
|||||||
CmdResult::ReportMeasurement(listing)
|
CmdResult::ReportMeasurement(listing)
|
||||||
}
|
}
|
||||||
Step::Ucs => match keyword(t, &["Yes", "No"]) {
|
Step::Ucs => match keyword(t, &["Yes", "No"]) {
|
||||||
Some(answer) => CmdResult::Dispatch(format!("-VIEW >UCSVIEW {}", u8::from(answer == "Yes"))),
|
Some(answer) => {
|
||||||
|
CmdResult::Dispatch(format!("-VIEW >UCSVIEW {}", u8::from(answer == "Yes")))
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
self.step = Step::Ucs;
|
self.step = Step::Ucs;
|
||||||
CmdResult::ReportError("Invalid option keyword.".into())
|
CmdResult::ReportError("Invalid option keyword.".into())
|
||||||
@@ -520,13 +596,23 @@ impl CadCommand for DashViewCommand {
|
|||||||
// pick well inside a viewport selects nothing, as in the
|
// pick well inside a viewport selects nothing, as in the
|
||||||
// reference.
|
// reference.
|
||||||
let tol = self.names.sheet_pick_tolerance.max(0.0);
|
let tol = self.names.sheet_pick_tolerance.max(0.0);
|
||||||
let hit = self.names.sheet_viewports.iter().flatten().rev().find(|(_, [x0, y0, x1, y1])| {
|
let hit = self.names.sheet_viewports.iter().flatten().rev().find(
|
||||||
let outside = pt.x < x0 - tol || pt.x > x1 + tol || pt.y < y0 - tol || pt.y > y1 + tol;
|
|(_, [x0, y0, x1, y1])| {
|
||||||
let inside = pt.x > x0 + tol && pt.x < x1 - tol && pt.y > y0 + tol && pt.y < y1 - tol;
|
let outside = pt.x < x0 - tol
|
||||||
!outside && !inside
|
|| pt.x > x1 + tol
|
||||||
});
|
|| pt.y < y0 - tol
|
||||||
|
|| pt.y > y1 + tol;
|
||||||
|
let inside = pt.x > x0 + tol
|
||||||
|
&& pt.x < x1 - tol
|
||||||
|
&& pt.y > y0 + tol
|
||||||
|
&& pt.y < y1 - tol;
|
||||||
|
!outside && !inside
|
||||||
|
},
|
||||||
|
);
|
||||||
match hit {
|
match hit {
|
||||||
Some((handle, _)) => CmdResult::Dispatch(format!("-VIEW >RESTOREVP {name}\u{1f}{handle:X}")),
|
Some((handle, _)) => {
|
||||||
|
CmdResult::Dispatch(format!("-VIEW >RESTOREVP {name}\u{1f}{handle:X}"))
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
self.step = Step::RestoreViewport(name);
|
self.step = Step::RestoreViewport(name);
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
@@ -542,12 +628,17 @@ impl CadCommand for DashViewCommand {
|
|||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
match std::mem::replace(&mut self.step, Step::Main) {
|
match std::mem::replace(&mut self.step, Step::Main) {
|
||||||
Step::Main | Step::Delete | Step::Restore | Step::RestoreViewport(_) | Step::Save | Step::WindowName => {
|
Step::Main
|
||||||
CmdResult::Dispatch("-VIEW >END".into())
|
| Step::Delete
|
||||||
}
|
| Step::Restore
|
||||||
|
| Step::RestoreViewport(_)
|
||||||
|
| Step::Save
|
||||||
|
| Step::WindowName => CmdResult::Dispatch("-VIEW >END".into()),
|
||||||
Step::Orthographic => CmdResult::Dispatch("-VIEW >PRESET TOP".into()),
|
Step::Orthographic => CmdResult::Dispatch("-VIEW >PRESET TOP".into()),
|
||||||
Step::List => CmdResult::Measurement(self.list_views("*")),
|
Step::List => CmdResult::Measurement(self.list_views("*")),
|
||||||
Step::WindowFirst(_) | Step::WindowSecond(..) => Self::end("Invalid window specification."),
|
Step::WindowFirst(_) | Step::WindowSecond(..) => {
|
||||||
|
Self::end("Invalid window specification.")
|
||||||
|
}
|
||||||
// Enter at the settings prompt goes back to the main prompt.
|
// Enter at the settings prompt goes back to the main prompt.
|
||||||
Step::Settings => CmdResult::NeedPoint,
|
Step::Settings => CmdResult::NeedPoint,
|
||||||
Step::SettingView(_) | Step::Ucs => {
|
Step::SettingView(_) | Step::Ucs => {
|
||||||
|
|||||||
+121
-47
@@ -80,12 +80,13 @@ pub fn array_of_insert(document: &CadDocument, insert: Handle) -> Option<AssocAr
|
|||||||
let Some(dependency) = dependency(document, link.dependency) else {
|
let Some(dependency) = dependency(document, link.dependency) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
match (dependency.is_read_dependency, dependency.is_write_dependency) {
|
match (
|
||||||
|
dependency.is_read_dependency,
|
||||||
|
dependency.is_write_dependency,
|
||||||
|
) {
|
||||||
(true, true) if dependency.dependent_on == insert => owns_insert = true,
|
(true, true) if dependency.dependent_on == insert => owns_insert = true,
|
||||||
(false, true) => array_block = dependency.dependent_on,
|
(false, true) => array_block = dependency.dependent_on,
|
||||||
(true, false) if source_block.is_null() => {
|
(true, false) if source_block.is_null() => source_block = dependency.dependent_on,
|
||||||
source_block = dependency.dependent_on
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,16 +96,14 @@ pub fn array_of_insert(document: &CadDocument, insert: Handle) -> Option<AssocAr
|
|||||||
"AcDbAssocArrayPathParameters" => ArrayKind::Path,
|
"AcDbAssocArrayPathParameters" => ArrayKind::Path,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
(owns_insert && !array_block.is_null() && !source_block.is_null()).then_some(
|
(owns_insert && !array_block.is_null() && !source_block.is_null()).then_some(AssocArray {
|
||||||
AssocArray {
|
insert,
|
||||||
insert,
|
action: handle,
|
||||||
action: handle,
|
body: action.action_body,
|
||||||
body: action.action_body,
|
array_block,
|
||||||
array_block,
|
source_block,
|
||||||
source_block,
|
kind,
|
||||||
kind,
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,8 +180,7 @@ pub fn column_axis(document: &CadDocument, array: &AssocArray) -> DVec3 {
|
|||||||
let AssociativeData::EdgeActionParam(edge) = &object.data else {
|
let AssociativeData::EdgeActionParam(edge) = &object.data else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
if edge.single_dependency.action_param.name != "RowProfile" || edge.action_type != 19
|
if edge.single_dependency.action_param.name != "RowProfile" || edge.action_type != 19 {
|
||||||
{
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
match edge.curve.get(1)? {
|
match edge.curve.get(1)? {
|
||||||
@@ -307,10 +305,17 @@ pub fn polar_items(
|
|||||||
get("Levels", 1.0).max(1.0) as i32,
|
get("Levels", 1.0).max(1.0) as i32,
|
||||||
);
|
);
|
||||||
// ponytail: Direction 0 read as clockwise; no sample with 0 to confirm.
|
// ponytail: Direction 0 read as clockwise; no sample with 0 to confirm.
|
||||||
let sign = if get("Direction", 1.0) == 0.0 { -1.0 } else { 1.0 };
|
let sign = if get("Direction", 1.0) == 0.0 {
|
||||||
|
-1.0
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
let align = get("AlignItems", 1.0) != 0.0;
|
let align = get("AlignItems", 1.0) != 0.0;
|
||||||
let (spacing, start, radius) =
|
let (spacing, start, radius) = (
|
||||||
(get("ItemSpacing", 0.0), get("StartAngle", 0.0), get("Radius", 0.0));
|
get("ItemSpacing", 0.0),
|
||||||
|
get("StartAngle", 0.0),
|
||||||
|
get("Radius", 0.0),
|
||||||
|
);
|
||||||
let x = plane_x(normal);
|
let x = plane_x(normal);
|
||||||
let y = normal.cross(x);
|
let y = normal.cross(x);
|
||||||
let start_dir = x * start.cos() + y * start.sin();
|
let start_dir = x * start.cos() + y * start.sin();
|
||||||
@@ -342,11 +347,21 @@ fn property_rows(kind: ArrayKind) -> &'static [(&'static str, &'static str, &'st
|
|||||||
match kind {
|
match kind {
|
||||||
ArrayKind::Rectangular => &[
|
ArrayKind::Rectangular => &[
|
||||||
("assoc_array_columns", "Items", "Columns", 0),
|
("assoc_array_columns", "Items", "Columns", 0),
|
||||||
("assoc_array_column_spacing", "ItemSpacing", "Column spacing", 1),
|
(
|
||||||
|
"assoc_array_column_spacing",
|
||||||
|
"ItemSpacing",
|
||||||
|
"Column spacing",
|
||||||
|
1,
|
||||||
|
),
|
||||||
("assoc_array_rows", "Rows", "Rows", 0),
|
("assoc_array_rows", "Rows", "Rows", 0),
|
||||||
("assoc_array_row_spacing", "RowSpacing", "Row spacing", 1),
|
("assoc_array_row_spacing", "RowSpacing", "Row spacing", 1),
|
||||||
("assoc_array_levels", "Levels", "Levels", 0),
|
("assoc_array_levels", "Levels", "Levels", 0),
|
||||||
("assoc_array_level_spacing", "LevelSpacing", "Level spacing", 1),
|
(
|
||||||
|
"assoc_array_level_spacing",
|
||||||
|
"LevelSpacing",
|
||||||
|
"Level spacing",
|
||||||
|
1,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
ArrayKind::Polar => &[
|
ArrayKind::Polar => &[
|
||||||
("assoc_array_items", "Items", "Items", 0),
|
("assoc_array_items", "Items", "Items", 0),
|
||||||
@@ -355,7 +370,12 @@ fn property_rows(kind: ArrayKind) -> &'static [(&'static str, &'static str, &'st
|
|||||||
("assoc_array_rows", "Rows", "Rows", 0),
|
("assoc_array_rows", "Rows", "Rows", 0),
|
||||||
("assoc_array_row_spacing", "RowSpacing", "Row spacing", 1),
|
("assoc_array_row_spacing", "RowSpacing", "Row spacing", 1),
|
||||||
("assoc_array_levels", "Levels", "Levels", 0),
|
("assoc_array_levels", "Levels", "Levels", 0),
|
||||||
("assoc_array_level_spacing", "LevelSpacing", "Level spacing", 1),
|
(
|
||||||
|
"assoc_array_level_spacing",
|
||||||
|
"LevelSpacing",
|
||||||
|
"Level spacing",
|
||||||
|
1,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
ArrayKind::Path => &[
|
ArrayKind::Path => &[
|
||||||
("assoc_array_items", "Items", "Items", 0),
|
("assoc_array_items", "Items", "Items", 0),
|
||||||
@@ -363,7 +383,12 @@ fn property_rows(kind: ArrayKind) -> &'static [(&'static str, &'static str, &'st
|
|||||||
("assoc_array_rows", "Rows", "Rows", 0),
|
("assoc_array_rows", "Rows", "Rows", 0),
|
||||||
("assoc_array_row_spacing", "RowSpacing", "Row spacing", 1),
|
("assoc_array_row_spacing", "RowSpacing", "Row spacing", 1),
|
||||||
("assoc_array_levels", "Levels", "Levels", 0),
|
("assoc_array_levels", "Levels", "Levels", 0),
|
||||||
("assoc_array_level_spacing", "LevelSpacing", "Level spacing", 1),
|
(
|
||||||
|
"assoc_array_level_spacing",
|
||||||
|
"LevelSpacing",
|
||||||
|
"Level spacing",
|
||||||
|
1,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,8 +442,7 @@ pub fn apply_field(
|
|||||||
field: &str,
|
field: &str,
|
||||||
text: &str,
|
text: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let Some(&(_, name, _, kind)) =
|
let Some(&(_, name, _, kind)) = property_rows(array.kind).iter().find(|row| row.0 == field)
|
||||||
property_rows(array.kind).iter().find(|row| row.0 == field)
|
|
||||||
else {
|
else {
|
||||||
return Err("unknown array field".into());
|
return Err("unknown array field".into());
|
||||||
};
|
};
|
||||||
@@ -460,7 +484,9 @@ pub fn update(
|
|||||||
set_param_value(&mut params, name, *value);
|
set_param_value(&mut params, name, *value);
|
||||||
}
|
}
|
||||||
if array.kind == ArrayKind::Polar
|
if array.kind == ArrayKind::Polar
|
||||||
&& changes.iter().any(|(name, _)| matches!(*name, "Items" | "FillAngle"))
|
&& changes
|
||||||
|
.iter()
|
||||||
|
.any(|(name, _)| matches!(*name, "Items" | "FillAngle"))
|
||||||
{
|
{
|
||||||
let fill = param_value(¶ms, "FillAngle").unwrap_or(std::f64::consts::TAU);
|
let fill = param_value(¶ms, "FillAngle").unwrap_or(std::f64::consts::TAU);
|
||||||
let count = param_value(¶ms, "Items").unwrap_or(1.0).max(1.0);
|
let count = param_value(¶ms, "Items").unwrap_or(1.0).max(1.0);
|
||||||
@@ -475,12 +501,10 @@ pub fn update(
|
|||||||
}
|
}
|
||||||
let normal = base_normal(document, array);
|
let normal = base_normal(document, array);
|
||||||
let placements: Vec<([i32; 3], DMat4)> = match array.kind {
|
let placements: Vec<([i32; 3], DMat4)> = match array.kind {
|
||||||
ArrayKind::Rectangular => {
|
ArrayKind::Rectangular => rectangular_items(¶ms, column_axis(document, array), normal)
|
||||||
rectangular_items(¶ms, column_axis(document, array), normal)
|
.into_iter()
|
||||||
.into_iter()
|
.map(|(location, offset)| (location, DMat4::from_translation(offset)))
|
||||||
.map(|(location, offset)| (location, DMat4::from_translation(offset)))
|
.collect(),
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
_ => polar_items(¶ms, center, normal),
|
_ => polar_items(¶ms, center, normal),
|
||||||
};
|
};
|
||||||
let current = body(document, array.body).ok_or("array body missing")?;
|
let current = body(document, array.body).ok_or("array body missing")?;
|
||||||
@@ -547,7 +571,9 @@ pub fn update(
|
|||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|record| record.handle == array.source_block)
|
.find(|record| record.handle == array.source_block)
|
||||||
{
|
{
|
||||||
record.insert_handles.retain(|handle| !old_items.contains(handle));
|
record
|
||||||
|
.insert_handles
|
||||||
|
.retain(|handle| !old_items.contains(handle));
|
||||||
record.insert_handles.extend(new_handles);
|
record.insert_handles.extend(new_handles);
|
||||||
}
|
}
|
||||||
let values_in_action = action(document, array.action).is_some_and(|a| !a.values.is_empty());
|
let values_in_action = action(document, array.action).is_some_and(|a| !a.values.is_empty());
|
||||||
@@ -581,11 +607,36 @@ const ARRAY_CLASSES: &[(&str, &str, i32, i32)] = &[
|
|||||||
("ACDBASSOCNETWORK", "AcDbAssocNetwork", 27, 45),
|
("ACDBASSOCNETWORK", "AcDbAssocNetwork", 27, 45),
|
||||||
("ACDBASSOCACTION", "AcDbAssocAction", 27, 45),
|
("ACDBASSOCACTION", "AcDbAssocAction", 27, 45),
|
||||||
("ACDBASSOCDEPENDENCY", "AcDbAssocDependency", 27, 50),
|
("ACDBASSOCDEPENDENCY", "AcDbAssocDependency", 27, 50),
|
||||||
("ACDBASSOCVERTEXACTIONPARAM", "AcDbAssocVertexActionParam", 27, 50),
|
(
|
||||||
("ACDBASSOCEDGEACTIONPARAM", "AcDbAssocEdgeActionParam", 27, 50),
|
"ACDBASSOCVERTEXACTIONPARAM",
|
||||||
("ACDBASSOCARRAYACTIONBODY", "AcDbAssocArrayActionBody", 29, 55),
|
"AcDbAssocVertexActionParam",
|
||||||
("ACDBASSOCARRAYRECTANGULARPARAMETERS", "AcDbAssocArrayRectangularParameters", 29, 55),
|
27,
|
||||||
("ACDBASSOCARRAYPOLARPARAMETERS", "AcDbAssocArrayPolarParameters", 29, 55),
|
50,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"ACDBASSOCEDGEACTIONPARAM",
|
||||||
|
"AcDbAssocEdgeActionParam",
|
||||||
|
27,
|
||||||
|
50,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"ACDBASSOCARRAYACTIONBODY",
|
||||||
|
"AcDbAssocArrayActionBody",
|
||||||
|
29,
|
||||||
|
55,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"ACDBASSOCARRAYRECTANGULARPARAMETERS",
|
||||||
|
"AcDbAssocArrayRectangularParameters",
|
||||||
|
29,
|
||||||
|
55,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"ACDBASSOCARRAYPOLARPARAMETERS",
|
||||||
|
"AcDbAssocArrayPolarParameters",
|
||||||
|
29,
|
||||||
|
55,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
fn register_array_classes(document: &mut CadDocument) {
|
fn register_array_classes(document: &mut CadDocument) {
|
||||||
@@ -644,9 +695,12 @@ fn dictionary_entry(document: &CadDocument, dictionary: Handle, key: &str) -> Op
|
|||||||
|
|
||||||
/// A dictionary stored under `key` in `parent`, created when absent.
|
/// A dictionary stored under `key` in `parent`, created when absent.
|
||||||
fn child_dictionary(document: &mut CadDocument, parent: Handle, key: &str) -> Handle {
|
fn child_dictionary(document: &mut CadDocument, parent: Handle, key: &str) -> Handle {
|
||||||
if let Some(handle) = dictionary_entry(document, parent, key)
|
if let Some(handle) = dictionary_entry(document, parent, key).filter(|handle| {
|
||||||
.filter(|handle| matches!(document.objects.get(handle), Some(ObjectType::Dictionary(_))))
|
matches!(
|
||||||
{
|
document.objects.get(handle),
|
||||||
|
Some(ObjectType::Dictionary(_))
|
||||||
|
)
|
||||||
|
}) {
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
let handle = document.allocate_handle();
|
let handle = document.allocate_handle();
|
||||||
@@ -654,7 +708,9 @@ fn child_dictionary(document: &mut CadDocument, parent: Handle, key: &str) -> Ha
|
|||||||
dictionary.handle = handle;
|
dictionary.handle = handle;
|
||||||
dictionary.owner = parent;
|
dictionary.owner = parent;
|
||||||
dictionary.reactors.push(parent);
|
dictionary.reactors.push(parent);
|
||||||
document.objects.insert(handle, ObjectType::Dictionary(dictionary));
|
document
|
||||||
|
.objects
|
||||||
|
.insert(handle, ObjectType::Dictionary(dictionary));
|
||||||
if let Some(ObjectType::Dictionary(parent)) = document.objects.get_mut(&parent) {
|
if let Some(ObjectType::Dictionary(parent)) = document.objects.get_mut(&parent) {
|
||||||
parent.add_entry(key, handle);
|
parent.add_entry(key, handle);
|
||||||
}
|
}
|
||||||
@@ -679,7 +735,12 @@ fn model_space_network(document: &mut CadDocument) -> Handle {
|
|||||||
}
|
}
|
||||||
let root_holder = child_dictionary(document, document.header.named_objects_dict_handle, KEY);
|
let root_holder = child_dictionary(document, document.header.named_objects_dict_handle, KEY);
|
||||||
let root = match dictionary_entry(document, root_holder, KEY) {
|
let root = match dictionary_entry(document, root_holder, KEY) {
|
||||||
Some(root) if matches!(document.objects.get(&root), Some(ObjectType::Associative(_))) => {
|
Some(root)
|
||||||
|
if matches!(
|
||||||
|
document.objects.get(&root),
|
||||||
|
Some(ObjectType::Associative(_))
|
||||||
|
) =>
|
||||||
|
{
|
||||||
root
|
root
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -896,7 +957,10 @@ pub fn create(
|
|||||||
scene.define_block_raw(
|
scene.define_block_raw(
|
||||||
&array_name,
|
&array_name,
|
||||||
codec::types::Vector3::ZERO,
|
codec::types::Vector3::ZERO,
|
||||||
placements.iter().map(|(_, placement)| item(*placement)).collect(),
|
placements
|
||||||
|
.iter()
|
||||||
|
.map(|(_, placement)| item(*placement))
|
||||||
|
.collect(),
|
||||||
);
|
);
|
||||||
let record = |document: &CadDocument, name: &str| {
|
let record = |document: &CadDocument, name: &str| {
|
||||||
document
|
document
|
||||||
@@ -930,8 +994,11 @@ pub fn create(
|
|||||||
let [action, body, write_dep, read_dep, both_dep] =
|
let [action, body, write_dep, read_dep, both_dep] =
|
||||||
std::array::from_fn(|_| document.allocate_handle());
|
std::array::from_fn(|_| document.allocate_handle());
|
||||||
let mut owned_parameters = Vec::new();
|
let mut owned_parameters = Vec::new();
|
||||||
let vertex_params =
|
let vertex_params = [
|
||||||
[("BaseNormal", normal), ("Origin", DVec3::ZERO), ("basePoint", DVec3::ZERO)];
|
("BaseNormal", normal),
|
||||||
|
("Origin", DVec3::ZERO),
|
||||||
|
("basePoint", DVec3::ZERO),
|
||||||
|
];
|
||||||
for (name, point) in vertex_params {
|
for (name, point) in vertex_params {
|
||||||
let handle = document.allocate_handle();
|
let handle = document.allocate_handle();
|
||||||
document.objects.insert(
|
document.objects.insert(
|
||||||
@@ -1109,7 +1176,14 @@ pub fn create_from_spec(
|
|||||||
offset.truncate().length(),
|
offset.truncate().length(),
|
||||||
offset.y.atan2(offset.x),
|
offset.y.atan2(offset.x),
|
||||||
);
|
);
|
||||||
create(scene, sources, source_point, center, ArrayKind::Polar, params)
|
create(
|
||||||
|
scene,
|
||||||
|
sources,
|
||||||
|
source_point,
|
||||||
|
center,
|
||||||
|
ArrayKind::Polar,
|
||||||
|
params,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,11 +35,23 @@ const NO_VALUE: i16 = -9999;
|
|||||||
/// (DXF name, class, DWG maintenance version) of the classes used here.
|
/// (DXF name, class, DWG maintenance version) of the classes used here.
|
||||||
const CLASSES: &[(&str, &str, i32)] = &[
|
const CLASSES: &[(&str, &str, i32)] = &[
|
||||||
("ACAD_EVALUATION_GRAPH", "AcDbEvalGraph", 55),
|
("ACAD_EVALUATION_GRAPH", "AcDbEvalGraph", 55),
|
||||||
("BLOCKVISIBILITYPARAMETER", "AcDbBlockVisibilityParameter", 55),
|
(
|
||||||
|
"BLOCKVISIBILITYPARAMETER",
|
||||||
|
"AcDbBlockVisibilityParameter",
|
||||||
|
55,
|
||||||
|
),
|
||||||
("BLOCKVISIBILITYGRIP", "AcDbBlockVisibilityGrip", 55),
|
("BLOCKVISIBILITYGRIP", "AcDbBlockVisibilityGrip", 55),
|
||||||
("BLOCKGRIPLOCATIONCOMPONENT", "AcDbBlockGripExpr", 20),
|
("BLOCKGRIPLOCATIONCOMPONENT", "AcDbBlockGripExpr", 20),
|
||||||
("ACDB_DYNAMICBLOCKPURGEPREVENTER_VERSION", "AcDbDynamicBlockPurgePreventer", 61),
|
(
|
||||||
("ACDB_BLOCKREPRESENTATION_DATA", "AcDbBlockRepresentationData", 58),
|
"ACDB_DYNAMICBLOCKPURGEPREVENTER_VERSION",
|
||||||
|
"AcDbDynamicBlockPurgePreventer",
|
||||||
|
61,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"ACDB_BLOCKREPRESENTATION_DATA",
|
||||||
|
"AcDbBlockRepresentationData",
|
||||||
|
58,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Register the classes with the flags and versions the reference writes
|
/// Register the classes with the flags and versions the reference writes
|
||||||
@@ -240,8 +252,7 @@ pub fn create(document: &mut CadDocument, definition: Handle, location: Vector3)
|
|||||||
ensure_app_id(document, app);
|
ensure_app_id(document, app);
|
||||||
}
|
}
|
||||||
let dictionary = document.ensure_extension_dictionary(definition);
|
let dictionary = document.ensure_extension_dictionary(definition);
|
||||||
let [graph, parameter, grip, x, y, preventer] =
|
let [graph, parameter, grip, x, y, preventer] = [(); 6].map(|_| document.allocate_handle());
|
||||||
[(); 6].map(|_| document.allocate_handle());
|
|
||||||
|
|
||||||
store(
|
store(
|
||||||
document,
|
document,
|
||||||
@@ -326,16 +337,15 @@ pub fn create(document: &mut CadDocument, definition: Handle, location: Vector3)
|
|||||||
node_data,
|
node_data,
|
||||||
active_cycles: None,
|
active_cycles: None,
|
||||||
};
|
};
|
||||||
let edge = |id: i32, source_node: i32, destination_node: i32, outgoing_edges| {
|
let edge =
|
||||||
BlockEvaluationEdge {
|
|id: i32, source_node: i32, destination_node: i32, outgoing_edges| BlockEvaluationEdge {
|
||||||
id,
|
id,
|
||||||
next_id: 0,
|
next_id: 0,
|
||||||
incoming_edge: 1,
|
incoming_edge: 1,
|
||||||
source_node,
|
source_node,
|
||||||
destination_node,
|
destination_node,
|
||||||
outgoing_edges,
|
outgoing_edges,
|
||||||
}
|
};
|
||||||
};
|
|
||||||
let mut graph_object = dynamic(
|
let mut graph_object = dynamic(
|
||||||
graph,
|
graph,
|
||||||
dictionary,
|
dictionary,
|
||||||
@@ -393,9 +403,11 @@ pub fn create(document: &mut CadDocument, definition: Handle, location: Vector3)
|
|||||||
/// The names of the states of `parameter`, in list order.
|
/// The names of the states of `parameter`, in list order.
|
||||||
pub fn state_names(document: &CadDocument, parameter: Handle) -> Vec<String> {
|
pub fn state_names(document: &CadDocument, parameter: Handle) -> Vec<String> {
|
||||||
match document.objects.get(¶meter) {
|
match document.objects.get(¶meter) {
|
||||||
Some(ObjectType::BlockVisibilityParameter(parameter)) => {
|
Some(ObjectType::BlockVisibilityParameter(parameter)) => parameter
|
||||||
parameter.states.iter().map(|state| state.name.clone()).collect()
|
.states
|
||||||
}
|
.iter()
|
||||||
|
.map(|state| state.name.clone())
|
||||||
|
.collect(),
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -597,7 +609,11 @@ impl Scene {
|
|||||||
let Some(EntityType::Insert(reference)) = self.document.get_entity(insert) else {
|
let Some(EntityType::Insert(reference)) = self.document.get_entity(insert) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let definition = self.document.block_records.get(&reference.block_name)?.handle;
|
let definition = self
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.get(&reference.block_name)?
|
||||||
|
.handle;
|
||||||
parameter(&self.document, definition)?;
|
parameter(&self.document, definition)?;
|
||||||
let name = {
|
let name = {
|
||||||
let next = self
|
let next = self
|
||||||
@@ -614,10 +630,12 @@ impl Scene {
|
|||||||
.block_records
|
.block_records
|
||||||
.iter()
|
.iter()
|
||||||
.find(|record| record.handle == definition)
|
.find(|record| record.handle == definition)
|
||||||
.and_then(|record| match self.document.get_entity(record.block_entity_handle) {
|
.and_then(
|
||||||
Some(EntityType::Block(block)) => Some(block.base_point),
|
|record| match self.document.get_entity(record.block_entity_handle) {
|
||||||
_ => None,
|
Some(EntityType::Block(block)) => Some(block.base_point),
|
||||||
})
|
_ => None,
|
||||||
|
},
|
||||||
|
)
|
||||||
.unwrap_or(Vector3::ZERO);
|
.unwrap_or(Vector3::ZERO);
|
||||||
let originals = members(&self.document, definition);
|
let originals = members(&self.document, definition);
|
||||||
let copies: Vec<EntityType> = originals
|
let copies: Vec<EntityType> = originals
|
||||||
@@ -701,7 +719,10 @@ mod tests {
|
|||||||
Vector3::ZERO,
|
Vector3::ZERO,
|
||||||
vec![
|
vec![
|
||||||
EntityType::Line(Line::from_coords(0.0, 0.0, 0.0, 1.0, 0.0, 0.0)),
|
EntityType::Line(Line::from_coords(0.0, 0.0, 0.0, 1.0, 0.0, 0.0)),
|
||||||
EntityType::Circle(Circle::from_center_radius(Vector3::new(0.5, 0.5, 0.0), 0.25)),
|
EntityType::Circle(Circle::from_center_radius(
|
||||||
|
Vector3::new(0.5, 0.5, 0.0),
|
||||||
|
0.25,
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
let definition = scene.document.block_records.get("door").unwrap().handle;
|
let definition = scene.document.block_records.get("door").unwrap().handle;
|
||||||
@@ -732,7 +753,11 @@ mod tests {
|
|||||||
let (found, parameter) = read
|
let (found, parameter) = read
|
||||||
.dynamic_visibility_for_insert(insert)
|
.dynamic_visibility_for_insert(insert)
|
||||||
.unwrap_or_else(|| panic!("{ext}: no visibility parameter"));
|
.unwrap_or_else(|| panic!("{ext}: no visibility parameter"));
|
||||||
let record = read.block_records.iter().find(|r| r.handle == found).unwrap();
|
let record = read
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.find(|r| r.handle == found)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(record.name, "door", "{ext}");
|
assert_eq!(record.name, "door", "{ext}");
|
||||||
let names: Vec<&str> = parameter.states.iter().map(|s| s.name.as_str()).collect();
|
let names: Vec<&str> = parameter.states.iter().map(|s| s.name.as_str()).collect();
|
||||||
assert_eq!(names, ["VisibilityState0", "Open"], "{ext}");
|
assert_eq!(names, ["VisibilityState0", "Open"], "{ext}");
|
||||||
|
|||||||
Vendored
+32
-32
@@ -1068,29 +1068,27 @@ pub fn expand_insert(
|
|||||||
LocalSub::Wire(wire) => wire.canvas_color,
|
LocalSub::Wire(wire) => wire.canvas_color,
|
||||||
LocalSub::Nested(_) => false,
|
LocalSub::Nested(_) => false,
|
||||||
});
|
});
|
||||||
let prototype_key = if !ins.is_array()
|
let prototype_key =
|
||||||
&& !has_masks
|
if !ins.is_array() && !has_masks && cache.prototype_blocks.contains(&ins.block_name) {
|
||||||
&& cache.prototype_blocks.contains(&ins.block_name)
|
Some(expansion_prototype_key(
|
||||||
{
|
ins,
|
||||||
Some(expansion_prototype_key(
|
&xform,
|
||||||
ins,
|
ins_resolved_color,
|
||||||
&xform,
|
ins_pat_len,
|
||||||
ins_resolved_color,
|
ins_pat,
|
||||||
ins_pat_len,
|
ins_lw_px,
|
||||||
ins_pat,
|
ins_layer,
|
||||||
ins_lw_px,
|
ins_layer_plottable,
|
||||||
ins_layer,
|
selected,
|
||||||
ins_layer_plottable,
|
pslt_factor,
|
||||||
selected,
|
is_xref,
|
||||||
pslt_factor,
|
bg_color,
|
||||||
is_xref,
|
anno_scale,
|
||||||
bg_color,
|
suppress_root_points,
|
||||||
anno_scale,
|
))
|
||||||
suppress_root_points,
|
} else {
|
||||||
))
|
None
|
||||||
} else {
|
};
|
||||||
None
|
|
||||||
};
|
|
||||||
let prototype_slot = prototype_key.as_ref().map(|key| {
|
let prototype_slot = prototype_key.as_ref().map(|key| {
|
||||||
let mut prototypes = cache
|
let mut prototypes = cache
|
||||||
.expansion_prototypes
|
.expansion_prototypes
|
||||||
@@ -1344,15 +1342,17 @@ pub(crate) fn translated_prototype_wire(
|
|||||||
let mut wire = source.clone();
|
let mut wire = source.clone();
|
||||||
wire.name = name.to_string();
|
wire.name = name.to_string();
|
||||||
if let Some(stub) = &source.stub {
|
if let Some(stub) = &source.stub {
|
||||||
wire.stub = Some(Arc::new(crate::scene::model::instance_model::InstanceStub {
|
wire.stub = Some(Arc::new(
|
||||||
prototype: Arc::clone(&stub.prototype),
|
crate::scene::model::instance_model::InstanceStub {
|
||||||
delta: [
|
prototype: Arc::clone(&stub.prototype),
|
||||||
stub.delta[0] + delta[0],
|
delta: [
|
||||||
stub.delta[1] + delta[1],
|
stub.delta[0] + delta[0],
|
||||||
stub.delta[2] + delta[2],
|
stub.delta[1] + delta[1],
|
||||||
],
|
stub.delta[2] + delta[2],
|
||||||
depth_delta: stub.depth_delta,
|
],
|
||||||
}));
|
depth_delta: stub.depth_delta,
|
||||||
|
},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if let Some(instance) = wire.render_instance.as_mut() {
|
if let Some(instance) = wire.render_instance.as_mut() {
|
||||||
for axis in 0..3 {
|
for axis in 0..3 {
|
||||||
|
|||||||
+79
-22
@@ -222,7 +222,9 @@ impl Scene {
|
|||||||
let unchanged = {
|
let unchanged = {
|
||||||
let cam = self.camera.borrow();
|
let cam = self.camera.borrow();
|
||||||
cam.rotation.abs_diff_eq(before.0, 1e-6)
|
cam.rotation.abs_diff_eq(before.0, 1e-6)
|
||||||
&& cam.target.abs_diff_eq(before.1, 1e-6 * before.1.length().max(1.0))
|
&& cam
|
||||||
|
.target
|
||||||
|
.abs_diff_eq(before.1, 1e-6 * before.1.length().max(1.0))
|
||||||
&& (cam.distance - before.2).abs() <= 1e-6 * before.2.abs().max(1.0)
|
&& (cam.distance - before.2).abs() <= 1e-6 * before.2.abs().max(1.0)
|
||||||
};
|
};
|
||||||
// The model viewport takes a copy of the view's background; a view
|
// The model viewport takes a copy of the view's background; a view
|
||||||
@@ -238,7 +240,9 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
if view.visual_style_handle.is_valid() {
|
if view.visual_style_handle.is_valid() {
|
||||||
if let Some(handle) = self.active_tile_vport_handle() {
|
if let Some(handle) = self.active_tile_vport_handle() {
|
||||||
if let Some(value) = self.document.vports.iter_mut().find(|v| v.handle == handle) {
|
if let Some(value) =
|
||||||
|
self.document.vports.iter_mut().find(|v| v.handle == handle)
|
||||||
|
{
|
||||||
value.visual_style_handle = view.visual_style_handle;
|
value.visual_style_handle = view.visual_style_handle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,7 +281,9 @@ impl Scene {
|
|||||||
let sheet = self.current_layout_sheet_viewport_handle();
|
let sheet = self.current_layout_sheet_viewport_handle();
|
||||||
let background = copy_background(&mut self.document, view.background_handle);
|
let background = copy_background(&mut self.document, view.background_handle);
|
||||||
let old = match self.document.get_entity_mut(sheet) {
|
let old = match self.document.get_entity_mut(sheet) {
|
||||||
Some(EntityType::Viewport(value)) => std::mem::replace(&mut value.background_handle, background),
|
Some(EntityType::Viewport(value)) => {
|
||||||
|
std::mem::replace(&mut value.background_handle, background)
|
||||||
|
}
|
||||||
_ => background,
|
_ => background,
|
||||||
};
|
};
|
||||||
erase_background(&mut self.document, old);
|
erase_background(&mut self.document, old);
|
||||||
@@ -324,36 +330,58 @@ impl Scene {
|
|||||||
if self.current_layout != "Model" {
|
if self.current_layout != "Model" {
|
||||||
// Inside a viewport, that viewport's; the sheet's is not saved
|
// Inside a viewport, that viewport's; the sheet's is not saved
|
||||||
// with a paper-space view.
|
// with a paper-space view.
|
||||||
return match self.active_viewport.and_then(|handle| self.document.get_entity(handle)) {
|
return match self
|
||||||
|
.active_viewport
|
||||||
|
.and_then(|handle| self.document.get_entity(handle))
|
||||||
|
{
|
||||||
Some(EntityType::Viewport(value)) => value.background_handle,
|
Some(EntityType::Viewport(value)) => value.background_handle,
|
||||||
_ => Handle::NULL,
|
_ => Handle::NULL,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
self.active_tile_vport_handle()
|
self.active_tile_vport_handle()
|
||||||
.and_then(|handle| self.document.vports.iter().find(|value| value.handle == handle))
|
.and_then(|handle| {
|
||||||
|
self.document
|
||||||
|
.vports
|
||||||
|
.iter()
|
||||||
|
.find(|value| value.handle == handle)
|
||||||
|
})
|
||||||
.map_or(Handle::NULL, |value| value.background_handle)
|
.map_or(Handle::NULL, |value| value.background_handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The visual style of the active Model tile's viewport record.
|
/// The visual style of the active Model tile's viewport record.
|
||||||
pub fn active_visual_style(&self) -> Handle {
|
pub fn active_visual_style(&self) -> Handle {
|
||||||
if self.current_layout != "Model" {
|
if self.current_layout != "Model" {
|
||||||
return match self.active_viewport.and_then(|handle| self.document.get_entity(handle)) {
|
return match self
|
||||||
|
.active_viewport
|
||||||
|
.and_then(|handle| self.document.get_entity(handle))
|
||||||
|
{
|
||||||
Some(EntityType::Viewport(value)) => value.visual_style_handle,
|
Some(EntityType::Viewport(value)) => value.visual_style_handle,
|
||||||
_ => Handle::NULL,
|
_ => Handle::NULL,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
self.active_tile_vport_handle()
|
self.active_tile_vport_handle()
|
||||||
.and_then(|handle| self.document.vports.iter().find(|value| value.handle == handle))
|
.and_then(|handle| {
|
||||||
|
self.document
|
||||||
|
.vports
|
||||||
|
.iter()
|
||||||
|
.find(|value| value.handle == handle)
|
||||||
|
})
|
||||||
.map_or(Handle::NULL, |value| value.visual_style_handle)
|
.map_or(Handle::NULL, |value| value.visual_style_handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Records the visual style named `name` on the active Model tile's
|
/// Records the visual style named `name` on the active Model tile's
|
||||||
/// viewport record, when the drawing has it.
|
/// viewport record, when the drawing has it.
|
||||||
pub fn set_active_visual_style_named(&mut self, name: &str) {
|
pub fn set_active_visual_style_named(&mut self, name: &str) {
|
||||||
let style = self.document.objects.iter().find_map(|(handle, object)| match object {
|
let style = self
|
||||||
codec::objects::ObjectType::VisualStyle(style) if style.description == name => Some(*handle),
|
.document
|
||||||
_ => None,
|
.objects
|
||||||
});
|
.iter()
|
||||||
|
.find_map(|(handle, object)| match object {
|
||||||
|
codec::objects::ObjectType::VisualStyle(style) if style.description == name => {
|
||||||
|
Some(*handle)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
let Some(style) = style else { return };
|
let Some(style) = style else { return };
|
||||||
if self.current_layout != "Model" {
|
if self.current_layout != "Model" {
|
||||||
if let Some(handle) = self.active_viewport {
|
if let Some(handle) = self.active_viewport {
|
||||||
@@ -363,15 +391,29 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(handle) = self.active_tile_vport_handle() else { return };
|
let Some(handle) = self.active_tile_vport_handle() else {
|
||||||
if let Some(value) = self.document.vports.iter_mut().find(|value| value.handle == handle) {
|
return;
|
||||||
|
};
|
||||||
|
if let Some(value) = self
|
||||||
|
.document
|
||||||
|
.vports
|
||||||
|
.iter_mut()
|
||||||
|
.find(|value| value.handle == handle)
|
||||||
|
{
|
||||||
value.visual_style_handle = style;
|
value.visual_style_handle = style;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_active_background(&mut self, background: Handle) {
|
fn set_active_background(&mut self, background: Handle) {
|
||||||
let Some(handle) = self.active_tile_vport_handle() else { return };
|
let Some(handle) = self.active_tile_vport_handle() else {
|
||||||
let Some(value) = self.document.vports.iter_mut().find(|value| value.handle == handle) else {
|
return;
|
||||||
|
};
|
||||||
|
let Some(value) = self
|
||||||
|
.document
|
||||||
|
.vports
|
||||||
|
.iter_mut()
|
||||||
|
.find(|value| value.handle == handle)
|
||||||
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let old = std::mem::replace(&mut value.background_handle, background);
|
let old = std::mem::replace(&mut value.background_handle, background);
|
||||||
@@ -1796,7 +1838,10 @@ fn background_dictionary(document: &mut codec::CadDocument) -> Option<Handle> {
|
|||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
if let Some(existing) = root_dictionary.get("ACAD_BACKGROUND") {
|
if let Some(existing) = root_dictionary.get("ACAD_BACKGROUND") {
|
||||||
if matches!(document.objects.get(&existing), Some(ObjectType::Dictionary(_))) {
|
if matches!(
|
||||||
|
document.objects.get(&existing),
|
||||||
|
Some(ObjectType::Dictionary(_))
|
||||||
|
) {
|
||||||
return Some(existing);
|
return Some(existing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1805,7 +1850,9 @@ fn background_dictionary(document: &mut codec::CadDocument) -> Option<Handle> {
|
|||||||
dictionary.handle = handle;
|
dictionary.handle = handle;
|
||||||
dictionary.owner = root;
|
dictionary.owner = root;
|
||||||
dictionary.reactors.push(root);
|
dictionary.reactors.push(root);
|
||||||
document.objects.insert(handle, ObjectType::Dictionary(dictionary));
|
document
|
||||||
|
.objects
|
||||||
|
.insert(handle, ObjectType::Dictionary(dictionary));
|
||||||
if let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get_mut(&root) {
|
if let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get_mut(&root) {
|
||||||
root_dictionary.add_entry("ACAD_BACKGROUND", handle);
|
root_dictionary.add_entry("ACAD_BACKGROUND", handle);
|
||||||
}
|
}
|
||||||
@@ -1827,7 +1874,9 @@ pub fn add_background(
|
|||||||
object.handle = handle;
|
object.handle = handle;
|
||||||
object.owner = dictionary;
|
object.owner = dictionary;
|
||||||
object.reactors = vec![dictionary];
|
object.reactors = vec![dictionary];
|
||||||
document.objects.insert(handle, ObjectType::ClassObject(object));
|
document
|
||||||
|
.objects
|
||||||
|
.insert(handle, ObjectType::ClassObject(object));
|
||||||
if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&dictionary) {
|
if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&dictionary) {
|
||||||
entries.add_entry(format!("{:X}", handle.value()), handle);
|
entries.add_entry(format!("{:X}", handle.value()), handle);
|
||||||
}
|
}
|
||||||
@@ -1852,8 +1901,12 @@ pub fn erase_background(document: &mut codec::CadDocument, handle: Handle) {
|
|||||||
Some(ObjectType::ClassObject(object))
|
Some(ObjectType::ClassObject(object))
|
||||||
if matches!(
|
if matches!(
|
||||||
object.data,
|
object.data,
|
||||||
D::SolidBackground(_) | D::GradientBackground(_) | D::ImageBackground(_)
|
D::SolidBackground(_)
|
||||||
| D::GroundPlaneBackground(_) | D::SkyLightBackground(_) | D::IblBackground(_)
|
| D::GradientBackground(_)
|
||||||
|
| D::ImageBackground(_)
|
||||||
|
| D::GroundPlaneBackground(_)
|
||||||
|
| D::SkyLightBackground(_)
|
||||||
|
| D::IblBackground(_)
|
||||||
) =>
|
) =>
|
||||||
{
|
{
|
||||||
object.owner
|
object.owner
|
||||||
@@ -1874,8 +1927,12 @@ pub fn copy_background(document: &mut codec::CadDocument, handle: Handle) -> Han
|
|||||||
Some(ObjectType::ClassObject(object))
|
Some(ObjectType::ClassObject(object))
|
||||||
if matches!(
|
if matches!(
|
||||||
object.data,
|
object.data,
|
||||||
D::SolidBackground(_) | D::GradientBackground(_) | D::ImageBackground(_)
|
D::SolidBackground(_)
|
||||||
| D::GroundPlaneBackground(_) | D::SkyLightBackground(_) | D::IblBackground(_)
|
| D::GradientBackground(_)
|
||||||
|
| D::ImageBackground(_)
|
||||||
|
| D::GroundPlaneBackground(_)
|
||||||
|
| D::SkyLightBackground(_)
|
||||||
|
| D::IblBackground(_)
|
||||||
) =>
|
) =>
|
||||||
{
|
{
|
||||||
let data = object.data.clone();
|
let data = object.data.clone();
|
||||||
|
|||||||
@@ -140,7 +140,11 @@ pub fn compare(current: &Scene, other: CadDocument) -> Comparison {
|
|||||||
b.1.y
|
b.1.y
|
||||||
.partial_cmp(&a.1.y)
|
.partial_cmp(&a.1.y)
|
||||||
.unwrap_or(std::cmp::Ordering::Equal)
|
.unwrap_or(std::cmp::Ordering::Equal)
|
||||||
.then(a.0.x.partial_cmp(&b.0.x).unwrap_or(std::cmp::Ordering::Equal))
|
.then(
|
||||||
|
a.0.x
|
||||||
|
.partial_cmp(&b.0.x)
|
||||||
|
.unwrap_or(std::cmp::Ordering::Equal),
|
||||||
|
)
|
||||||
});
|
});
|
||||||
Comparison {
|
Comparison {
|
||||||
added,
|
added,
|
||||||
|
|||||||
@@ -3211,10 +3211,9 @@ fn polyline_band_width(entity: &EntityType, fill_mode: bool) -> f32 {
|
|||||||
/// The widest start / end width of a 2-D polyline (0 for other entities).
|
/// The widest start / end width of a 2-D polyline (0 for other entities).
|
||||||
fn polyline_max_width(entity: &EntityType) -> f64 {
|
fn polyline_max_width(entity: &EntityType) -> f64 {
|
||||||
match entity {
|
match entity {
|
||||||
EntityType::LwPolyline(p) => p
|
EntityType::LwPolyline(p) => p.vertices.iter().fold(p.constant_width, |w, v| {
|
||||||
.vertices
|
w.max(v.start_width).max(v.end_width)
|
||||||
.iter()
|
}),
|
||||||
.fold(p.constant_width, |w, v| w.max(v.start_width).max(v.end_width)),
|
|
||||||
EntityType::Polyline2D(p) => p
|
EntityType::Polyline2D(p) => p
|
||||||
.vertices
|
.vertices
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
+406
-118
@@ -2494,9 +2494,7 @@ fn detach_preserved_scope(
|
|||||||
AssociativeData::Action(_) => {
|
AssociativeData::Action(_) => {
|
||||||
action_surface(document, *handle)
|
action_surface(document, *handle)
|
||||||
.is_none_or(|surface| document.get_entity(surface).is_some())
|
.is_none_or(|surface| document.get_entity(surface).is_some())
|
||||||
&& !crate::scene::assoc_array::orphaned_array_action(
|
&& !crate::scene::assoc_array::orphaned_array_action(document, *handle)
|
||||||
document, *handle,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
_ => true,
|
_ => true,
|
||||||
},
|
},
|
||||||
@@ -2668,9 +2666,13 @@ fn pending_swept_surfaces(document: &CadDocument) -> Vec<(Handle, Handle, Handle
|
|||||||
let (profile, path) = sweep_model::sweep_sources(entity)?;
|
let (profile, path) = sweep_model::sweep_sources(entity)?;
|
||||||
let surface = entity.common().handle;
|
let surface = entity.common().handle;
|
||||||
(!carried.contains(&surface)
|
(!carried.contains(&surface)
|
||||||
&& document.get_entity(profile).is_some_and(|e| sweep_model::assoc_source_supported(e))
|
&& document
|
||||||
&& document.get_entity(path).is_some_and(|e| sweep_model::assoc_source_supported(e)))
|
.get_entity(profile)
|
||||||
.then_some((surface, profile, path))
|
.is_some_and(|e| sweep_model::assoc_source_supported(e))
|
||||||
|
&& document
|
||||||
|
.get_entity(path)
|
||||||
|
.is_some_and(|e| sweep_model::assoc_source_supported(e)))
|
||||||
|
.then_some((surface, profile, path))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
pending.sort_by_key(|(surface, _, _)| surface.value());
|
pending.sort_by_key(|(surface, _, _)| surface.value());
|
||||||
@@ -2696,7 +2698,9 @@ fn pending_source_surfaces(
|
|||||||
let surface = entity.common().handle;
|
let surface = entity.common().handle;
|
||||||
(!carried.contains(&surface)
|
(!carried.contains(&surface)
|
||||||
&& sources.handles().iter().all(|handle| {
|
&& sources.handles().iter().all(|handle| {
|
||||||
document.get_entity(*handle).is_some_and(|e| sweep_model::assoc_source_supported(e))
|
document
|
||||||
|
.get_entity(*handle)
|
||||||
|
.is_some_and(|e| sweep_model::assoc_source_supported(e))
|
||||||
}))
|
}))
|
||||||
.then_some((surface, sources))
|
.then_some((surface, sources))
|
||||||
})
|
})
|
||||||
@@ -2733,11 +2737,20 @@ fn read_dependency(dependent_on: Handle, id: i32) -> AssocDependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An edge action parameter reading `curve` through dependency `read`.
|
/// An edge action parameter reading `curve` through dependency `read`.
|
||||||
fn edge_action_param(read: Handle, (action_type, subcurve_kind, curve): crate::scene::model::sweep_model::AssocCurve) -> AssociativeData {
|
fn edge_action_param(
|
||||||
use codec::objects::{AssocActionParam, AssocEdgeActionParam, AssocSingleDependencyActionParam};
|
read: Handle,
|
||||||
|
(action_type, subcurve_kind, curve): crate::scene::model::sweep_model::AssocCurve,
|
||||||
|
) -> AssociativeData {
|
||||||
|
use codec::objects::{
|
||||||
|
AssocActionParam, AssocEdgeActionParam, AssocSingleDependencyActionParam,
|
||||||
|
};
|
||||||
AssociativeData::EdgeActionParam(AssocEdgeActionParam {
|
AssociativeData::EdgeActionParam(AssocEdgeActionParam {
|
||||||
single_dependency: AssocSingleDependencyActionParam {
|
single_dependency: AssocSingleDependencyActionParam {
|
||||||
action_param: AssocActionParam { is_r2013: 1, version: 0, name: String::new() },
|
action_param: AssocActionParam {
|
||||||
|
is_r2013: 1,
|
||||||
|
version: 0,
|
||||||
|
name: String::new(),
|
||||||
|
},
|
||||||
dependency_class_version: 0,
|
dependency_class_version: 0,
|
||||||
dependency: read,
|
dependency: read,
|
||||||
class_version: 0,
|
class_version: 0,
|
||||||
@@ -2761,15 +2774,61 @@ fn region_subent_entries(loops: &[usize], step: i32, first_id: i32) -> Vec<i32>
|
|||||||
let (mut id, mut offset) = (first_id, 0usize);
|
let (mut id, mut offset) = (first_id, 0usize);
|
||||||
for &count in loops {
|
for &count in loops {
|
||||||
let edge = |index: usize| 51001 + 100_000 * (offset + index % count) as i32;
|
let edge = |index: usize| 51001 + 100_000 * (offset + index % count) as i32;
|
||||||
let vertex = |a: i32, b: i32| [1, 3, 1, BODY, FACE, 1, BODY, a, 1, BODY, b, 0, 0, 0, 900, 0, 900, 0, 0];
|
let vertex = |a: i32, b: i32| {
|
||||||
|
[
|
||||||
|
1, 3, 1, BODY, FACE, 1, BODY, a, 1, BODY, b, 0, 0, 0, 900, 0, 900, 0, 0,
|
||||||
|
]
|
||||||
|
};
|
||||||
for read in 0..count {
|
for read in 0..count {
|
||||||
if count == 1 {
|
if count == 1 {
|
||||||
values.extend([4, 0, 5, id, step, step, 0, 0, 24, 0, 0, 1, BODY, edge(0), 0, 0, 1, 1, BODY, FACE]);
|
values.extend([
|
||||||
|
4,
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
id,
|
||||||
|
step,
|
||||||
|
step,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
24,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
BODY,
|
||||||
|
edge(0),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
BODY,
|
||||||
|
FACE,
|
||||||
|
]);
|
||||||
values.extend([0; 14]);
|
values.extend([0; 14]);
|
||||||
} else {
|
} else {
|
||||||
// The dependencies start at the loop's second edge.
|
// The dependencies start at the loop's second edge.
|
||||||
let index = read + 1;
|
let index = read + 1;
|
||||||
values.extend([4, 0, 11, id, step, step, 0, 2, 2, 2, 0, 2, 2, 2, 54, 0, 0, 1, BODY, edge(index)]);
|
values.extend([
|
||||||
|
4,
|
||||||
|
0,
|
||||||
|
11,
|
||||||
|
id,
|
||||||
|
step,
|
||||||
|
step,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
54,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
BODY,
|
||||||
|
edge(index),
|
||||||
|
]);
|
||||||
values.extend([0, 0, 1, 1, BODY, FACE, 0, 0, 0, 0]);
|
values.extend([0, 0, 1, 1, BODY, FACE, 0, 0, 0, 0]);
|
||||||
values.extend(vertex(edge(index), edge(index + count - 1)));
|
values.extend(vertex(edge(index), edge(index + count - 1)));
|
||||||
values.extend([0, 0]);
|
values.extend([0, 0]);
|
||||||
@@ -2796,9 +2855,10 @@ fn register_region_subents(document: &mut CadDocument, loops: &[usize]) -> Optio
|
|||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
let current = existing.and_then(|handle| match document.objects.get(&handle) {
|
let current = existing.and_then(|handle| match document.objects.get(&handle) {
|
||||||
Some(ObjectType::Associative(AssociativeObject { data: AssociativeData::PersSubentManager(manager), .. })) => {
|
Some(ObjectType::Associative(AssociativeObject {
|
||||||
Some((handle, manager.values.clone()))
|
data: AssociativeData::PersSubentManager(manager),
|
||||||
}
|
..
|
||||||
|
})) => Some((handle, manager.values.clone())),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
let (handle, last, count, mut entries) = match current {
|
let (handle, last, count, mut entries) = match current {
|
||||||
@@ -2811,7 +2871,12 @@ fn register_region_subents(document: &mut CadDocument, loops: &[usize]) -> Optio
|
|||||||
if values.len() < tail + 2 || values[values.len() - tail] != last {
|
if values.len() < tail + 2 || values[values.len() - tail] != last {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
(handle, last, values[1], values[2..values.len() - tail].to_vec())
|
(
|
||||||
|
handle,
|
||||||
|
last,
|
||||||
|
values[1],
|
||||||
|
values[2..values.len() - tail].to_vec(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -2829,20 +2894,23 @@ fn register_region_subents(document: &mut CadDocument, loops: &[usize]) -> Optio
|
|||||||
values.push(last);
|
values.push(last);
|
||||||
values.extend(1..=last);
|
values.extend(1..=last);
|
||||||
values.extend([0, 0]);
|
values.extend([0, 0]);
|
||||||
document.objects.insert(handle, ObjectType::Associative(AssociativeObject {
|
document.objects.insert(
|
||||||
handle,
|
handle,
|
||||||
owner: root,
|
ObjectType::Associative(AssociativeObject {
|
||||||
reactors: vec![root],
|
handle,
|
||||||
dxf_name: "ACDBASSOCPERSSUBENTMANAGER".to_string(),
|
owner: root,
|
||||||
cpp_class_name: "AcDbAssocPersSubentManager".to_string(),
|
reactors: vec![root],
|
||||||
data: AssociativeData::PersSubentManager(AssocPersSubentManager {
|
dxf_name: "ACDBASSOCPERSSUBENTMANAGER".to_string(),
|
||||||
class_version: 2,
|
cpp_class_name: "AcDbAssocPersSubentManager".to_string(),
|
||||||
markers: [3, 0, 2],
|
data: AssociativeData::PersSubentManager(AssocPersSubentManager {
|
||||||
values,
|
class_version: 2,
|
||||||
final_flag: false,
|
markers: [3, 0, 2],
|
||||||
|
values,
|
||||||
|
final_flag: false,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
);
|
||||||
}));
|
|
||||||
Some((first..first + edges).collect())
|
Some((first..first + edges).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2864,9 +2932,20 @@ fn source_reads(
|
|||||||
let Some(loops) = sweep_model::region_edge_curves(entity) else {
|
let Some(loops) = sweep_model::region_edge_curves(entity) else {
|
||||||
let curve = sweep_model::assoc_edge_curve(entity)?;
|
let curve = sweep_model::assoc_edge_curve(entity)?;
|
||||||
let (edge, read) = (document.allocate_handle(), document.allocate_handle());
|
let (edge, read) = (document.allocate_handle(), document.allocate_handle());
|
||||||
objects.push((edge, parameter, "ACDBASSOCEDGEACTIONPARAM", "AcDbAssocEdgeActionParam", edge_action_param(read, curve)));
|
objects.push((
|
||||||
objects.push((read, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency",
|
edge,
|
||||||
AssociativeData::Dependency(read_dependency(source, first_id))));
|
parameter,
|
||||||
|
"ACDBASSOCEDGEACTIONPARAM",
|
||||||
|
"AcDbAssocEdgeActionParam",
|
||||||
|
edge_action_param(read, curve),
|
||||||
|
));
|
||||||
|
objects.push((
|
||||||
|
read,
|
||||||
|
action,
|
||||||
|
"ACDBASSOCDEPENDENCY",
|
||||||
|
"AcDbAssocDependency",
|
||||||
|
AssociativeData::Dependency(read_dependency(source, first_id)),
|
||||||
|
));
|
||||||
return Some(vec![(edge, read)]);
|
return Some(vec![(edge, read)]);
|
||||||
};
|
};
|
||||||
let sizes = loops.iter().map(Vec::len).collect::<Vec<_>>();
|
let sizes = loops.iter().map(Vec::len).collect::<Vec<_>>();
|
||||||
@@ -2874,16 +2953,36 @@ fn source_reads(
|
|||||||
// The region carries the step its edge names belong to; without it (and
|
// The region carries the step its edge names belong to; without it (and
|
||||||
// without named edges in its modeler data) the reference cannot find
|
// without named edges in its modeler data) the reference cannot find
|
||||||
// the edges again.
|
// the edges again.
|
||||||
crate::scene::view::dispatch::set_entity_xdata(document, source, "ACAD_STEPID",
|
crate::scene::view::dispatch::set_entity_xdata(
|
||||||
Some(vec![codec::xdata::XDataValue::Integer32(ids.first()? - 1)]));
|
document,
|
||||||
|
source,
|
||||||
|
"ACAD_STEPID",
|
||||||
|
Some(vec![codec::xdata::XDataValue::Integer32(ids.first()? - 1)]),
|
||||||
|
);
|
||||||
let curves = loops.into_iter().flatten().collect::<Vec<_>>();
|
let curves = loops.into_iter().flatten().collect::<Vec<_>>();
|
||||||
let pairs = curves.iter().map(|_| (document.allocate_handle(), document.allocate_handle())).collect::<Vec<_>>();
|
let pairs = curves
|
||||||
for (index, (curve, (edge, read))) in curves.into_iter().zip(pairs.iter().copied()).enumerate() {
|
.iter()
|
||||||
|
.map(|_| (document.allocate_handle(), document.allocate_handle()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for (index, (curve, (edge, read))) in curves.into_iter().zip(pairs.iter().copied()).enumerate()
|
||||||
|
{
|
||||||
let mut dependency = read_dependency(source, first_id + index as i32);
|
let mut dependency = read_dependency(source, first_id + index as i32);
|
||||||
dependency.read_dependency = index.checked_sub(1).map_or(Handle::NULL, |previous| pairs[previous].1);
|
dependency.read_dependency = index
|
||||||
|
.checked_sub(1)
|
||||||
|
.map_or(Handle::NULL, |previous| pairs[previous].1);
|
||||||
dependency.node = pairs.get(index + 1).map_or(Handle::NULL, |next| next.1);
|
dependency.node = pairs.get(index + 1).map_or(Handle::NULL, |next| next.1);
|
||||||
objects.push((edge, parameter, "ACDBASSOCEDGEACTIONPARAM", "AcDbAssocEdgeActionParam", edge_action_param(read, curve)));
|
objects.push((
|
||||||
objects.push((read, action, "ACDBASSOCGEOMDEPENDENCY", "AcDbAssocGeomDependency",
|
edge,
|
||||||
|
parameter,
|
||||||
|
"ACDBASSOCEDGEACTIONPARAM",
|
||||||
|
"AcDbAssocEdgeActionParam",
|
||||||
|
edge_action_param(read, curve),
|
||||||
|
));
|
||||||
|
objects.push((
|
||||||
|
read,
|
||||||
|
action,
|
||||||
|
"ACDBASSOCGEOMDEPENDENCY",
|
||||||
|
"AcDbAssocGeomDependency",
|
||||||
AssociativeData::GeomDependency(AssocGeomDependency {
|
AssociativeData::GeomDependency(AssocGeomDependency {
|
||||||
dependency,
|
dependency,
|
||||||
class_version: 0,
|
class_version: 0,
|
||||||
@@ -2895,7 +2994,8 @@ fn source_reads(
|
|||||||
values: vec![0, 2, ids[index]],
|
values: vec![0, 2, ids[index]],
|
||||||
leading_flag: false,
|
leading_flag: false,
|
||||||
},
|
},
|
||||||
})));
|
}),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Some(pairs)
|
Some(pairs)
|
||||||
}
|
}
|
||||||
@@ -2920,8 +3020,14 @@ fn materialize_source_surface_actions(
|
|||||||
let document = &*allocator.document;
|
let document = &*allocator.document;
|
||||||
// The value parameters: name, unit type, values, and whether they
|
// The value parameters: name, unit type, values, and whether they
|
||||||
// control the surface.
|
// control the surface.
|
||||||
let real = |value: f64| AssocEvalVariant { code: 40, value: AssocEvalValue::Real(value) };
|
let real = |value: f64| AssocEvalVariant {
|
||||||
let short = |value: i32| AssocEvalVariant { code: 70, value: AssocEvalValue::Short(value as i16) };
|
code: 40,
|
||||||
|
value: AssocEvalValue::Real(value),
|
||||||
|
};
|
||||||
|
let short = |value: i32| AssocEvalVariant {
|
||||||
|
code: 70,
|
||||||
|
value: AssocEvalValue::Short(value as i16),
|
||||||
|
};
|
||||||
let (kind, class, dxf, values, params): (
|
let (kind, class, dxf, values, params): (
|
||||||
AssocSurfaceActionKind,
|
AssocSurfaceActionKind,
|
||||||
&str,
|
&str,
|
||||||
@@ -2929,14 +3035,27 @@ fn materialize_source_surface_actions(
|
|||||||
Vec<(&str, i32, Vec<AssocEvalVariant>, bool)>,
|
Vec<(&str, i32, Vec<AssocEvalVariant>, bool)>,
|
||||||
Vec<(&str, Option<Handle>)>,
|
Vec<(&str, Option<Handle>)>,
|
||||||
) = match sources {
|
) = match sources {
|
||||||
SurfaceSources::Extrude { profile, height, taper, .. } => (
|
SurfaceSources::Extrude {
|
||||||
|
profile,
|
||||||
|
height,
|
||||||
|
taper,
|
||||||
|
..
|
||||||
|
} => (
|
||||||
AssocSurfaceActionKind::Extruded,
|
AssocSurfaceActionKind::Extruded,
|
||||||
"AcDbAssocExtrudedSurfaceActionBody",
|
"AcDbAssocExtrudedSurfaceActionBody",
|
||||||
"ACDBASSOCEXTRUDEDSURFACEACTIONBODY",
|
"ACDBASSOCEXTRUDEDSURFACEACTIONBODY",
|
||||||
vec![("ExtrusionHeight", 1, vec![real(*height)], true), ("ExtrusionTaperAngle", 2, vec![real(*taper)], true)],
|
vec![
|
||||||
|
("ExtrusionHeight", 1, vec![real(*height)], true),
|
||||||
|
("ExtrusionTaperAngle", 2, vec![real(*taper)], true),
|
||||||
|
],
|
||||||
vec![("ExtrudeProfile", Some(*profile))],
|
vec![("ExtrudeProfile", Some(*profile))],
|
||||||
),
|
),
|
||||||
SurfaceSources::Revolve { profile, axis, angle, .. } => (
|
SurfaceSources::Revolve {
|
||||||
|
profile,
|
||||||
|
axis,
|
||||||
|
angle,
|
||||||
|
..
|
||||||
|
} => (
|
||||||
AssocSurfaceActionKind::Revolved,
|
AssocSurfaceActionKind::Revolved,
|
||||||
"AcDbAssocRevolvedSurfaceActionBody",
|
"AcDbAssocRevolvedSurfaceActionBody",
|
||||||
"ACDBASSOCREVOLVEDSURFACEACTIONBODY",
|
"ACDBASSOCREVOLVEDSURFACEACTIONBODY",
|
||||||
@@ -2944,14 +3063,21 @@ fn materialize_source_surface_actions(
|
|||||||
vec![("RevolveProfile", Some(*profile)), ("RevolvePath", *axis)],
|
vec![("RevolveProfile", Some(*profile)), ("RevolvePath", *axis)],
|
||||||
),
|
),
|
||||||
SurfaceSources::Loft { sections } => {
|
SurfaceSources::Loft { sections } => {
|
||||||
let loft = document.solid_history_operations(surface).and_then(|operations| {
|
let loft = document
|
||||||
operations.into_iter().find_map(|operation| match operation {
|
.solid_history_operations(surface)
|
||||||
SolidHistoryOperation::Loft(record) => record.parameters,
|
.and_then(|operations| {
|
||||||
_ => None,
|
operations
|
||||||
})
|
.into_iter()
|
||||||
});
|
.find_map(|operation| match operation {
|
||||||
|
SolidHistoryOperation::Loft(record) => record.parameters,
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
});
|
||||||
let (continuity, bulge) = loft.map_or(((1, 1), (0.5, 0.5)), |p| {
|
let (continuity, bulge) = loft.map_or(((1, 1), (0.5, 0.5)), |p| {
|
||||||
((p.start_continuity, p.end_continuity), (p.start_bulge, p.end_bulge))
|
(
|
||||||
|
(p.start_continuity, p.end_continuity),
|
||||||
|
(p.start_bulge, p.end_bulge),
|
||||||
|
)
|
||||||
});
|
});
|
||||||
(
|
(
|
||||||
AssocSurfaceActionKind::Lofted,
|
AssocSurfaceActionKind::Lofted,
|
||||||
@@ -2960,30 +3086,84 @@ fn materialize_source_surface_actions(
|
|||||||
// One value per cross section: the start and end
|
// One value per cross section: the start and end
|
||||||
// settings at the ends, the defaults between them.
|
// settings at the ends, the defaults between them.
|
||||||
vec![
|
vec![
|
||||||
("Continuity", 0, (0..sections.len()).map(|index| short(if index == 0 { continuity.0 } else if index + 1 == sections.len() { continuity.1 } else { 1 })).collect(), false),
|
(
|
||||||
("Bulge", 0, (0..sections.len()).map(|index| real(if index == 0 { bulge.0 } else if index + 1 == sections.len() { bulge.1 } else { 0.5 })).collect(), false),
|
"Continuity",
|
||||||
|
0,
|
||||||
|
(0..sections.len())
|
||||||
|
.map(|index| {
|
||||||
|
short(if index == 0 {
|
||||||
|
continuity.0
|
||||||
|
} else if index + 1 == sections.len() {
|
||||||
|
continuity.1
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Bulge",
|
||||||
|
0,
|
||||||
|
(0..sections.len())
|
||||||
|
.map(|index| {
|
||||||
|
real(if index == 0 {
|
||||||
|
bulge.0
|
||||||
|
} else if index + 1 == sections.len() {
|
||||||
|
bulge.1
|
||||||
|
} else {
|
||||||
|
0.5
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
sections.iter().map(|section| ("CrossSection", Some(*section)))
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|section| ("CrossSection", Some(*section)))
|
||||||
.chain(std::iter::once(("PathCurve", None)))
|
.chain(std::iter::once(("PathCurve", None)))
|
||||||
.collect(),
|
.collect(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let document = &mut *allocator.document;
|
let document = &mut *allocator.document;
|
||||||
let (action, body, written) = (document.allocate_handle(), document.allocate_handle(), document.allocate_handle());
|
let (action, body, written) = (
|
||||||
let mut objects: Vec<(Handle, Handle, &'static str, &'static str, AssociativeData)> = Vec::new();
|
document.allocate_handle(),
|
||||||
|
document.allocate_handle(),
|
||||||
|
document.allocate_handle(),
|
||||||
|
);
|
||||||
|
let mut objects: Vec<(Handle, Handle, &'static str, &'static str, AssociativeData)> =
|
||||||
|
Vec::new();
|
||||||
let mut reactors: Vec<(Handle, Handle)> = vec![(surface, written)];
|
let mut reactors: Vec<(Handle, Handle)> = vec![(surface, written)];
|
||||||
let mut dependencies = vec![written];
|
let mut dependencies = vec![written];
|
||||||
let mut owned = Vec::new();
|
let mut owned = Vec::new();
|
||||||
let mut writes = read_dependency(surface, 1);
|
let mut writes = read_dependency(surface, 1);
|
||||||
(writes.is_read_dependency, writes.is_write_dependency, writes.order) = (false, true, i32::MIN);
|
(
|
||||||
objects.push((written, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", AssociativeData::Dependency(writes)));
|
writes.is_read_dependency,
|
||||||
|
writes.is_write_dependency,
|
||||||
|
writes.order,
|
||||||
|
) = (false, true, i32::MIN);
|
||||||
|
objects.push((
|
||||||
|
written,
|
||||||
|
action,
|
||||||
|
"ACDBASSOCDEPENDENCY",
|
||||||
|
"AcDbAssocDependency",
|
||||||
|
AssociativeData::Dependency(writes),
|
||||||
|
));
|
||||||
let mut complete = true;
|
let mut complete = true;
|
||||||
for (name, source) in params.iter() {
|
for (name, source) in params.iter() {
|
||||||
let parameter = document.allocate_handle();
|
let parameter = document.allocate_handle();
|
||||||
let mut children = Vec::new();
|
let mut children = Vec::new();
|
||||||
if let Some(source) = source {
|
if let Some(source) = source {
|
||||||
let Some(reads) = source_reads(document, action, parameter, *source, dependencies.len() as i32 + 1, &mut objects) else {
|
let Some(reads) = source_reads(
|
||||||
|
document,
|
||||||
|
action,
|
||||||
|
parameter,
|
||||||
|
*source,
|
||||||
|
dependencies.len() as i32 + 1,
|
||||||
|
&mut objects,
|
||||||
|
) else {
|
||||||
complete = false;
|
complete = false;
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
@@ -2993,48 +3173,88 @@ fn materialize_source_surface_actions(
|
|||||||
children.push(edge);
|
children.push(edge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
objects.push((parameter, action, "ACDBASSOCPATHACTIONPARAM", "AcDbAssocPathActionParam",
|
objects.push((
|
||||||
|
parameter,
|
||||||
|
action,
|
||||||
|
"ACDBASSOCPATHACTIONPARAM",
|
||||||
|
"AcDbAssocPathActionParam",
|
||||||
AssociativeData::PathActionParam(AssocPathActionParam {
|
AssociativeData::PathActionParam(AssocPathActionParam {
|
||||||
compound: AssocCompoundActionParam {
|
compound: AssocCompoundActionParam {
|
||||||
action_param: AssocActionParam { is_r2013: 1, version: 0, name: name.to_string() },
|
action_param: AssocActionParam {
|
||||||
|
is_r2013: 1,
|
||||||
|
version: 0,
|
||||||
|
name: name.to_string(),
|
||||||
|
},
|
||||||
class_version: 0,
|
class_version: 0,
|
||||||
status: 0,
|
status: 0,
|
||||||
parameters: children,
|
parameters: children,
|
||||||
child_parameter: None,
|
child_parameter: None,
|
||||||
},
|
},
|
||||||
version: 0,
|
version: 0,
|
||||||
})));
|
}),
|
||||||
|
));
|
||||||
owned.push(parameter);
|
owned.push(parameter);
|
||||||
}
|
}
|
||||||
if !complete {
|
if !complete {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
objects.push((action, network, "ACDBASSOCACTION", "AcDbAssocAction", AssociativeData::Action(AssocAction {
|
objects.push((
|
||||||
class_version: 2,
|
action,
|
||||||
geometry_status: 0,
|
network,
|
||||||
owning_network: network,
|
"ACDBASSOCACTION",
|
||||||
action_body: body,
|
"AcDbAssocAction",
|
||||||
action_index: 0,
|
AssociativeData::Action(AssocAction {
|
||||||
max_dependency_index: dependencies.len() as i32,
|
class_version: 2,
|
||||||
dependencies: dependencies.iter().map(|handle| AssocActionDependency { is_owned: true, dependency: *handle }).collect(),
|
geometry_status: 0,
|
||||||
owned_parameters: owned,
|
owning_network: network,
|
||||||
values: values
|
action_body: body,
|
||||||
.into_iter()
|
action_index: 0,
|
||||||
.map(|(name, unit_type, amounts, controls)| AssocValueParam {
|
max_dependency_index: dependencies.len() as i32,
|
||||||
class_version: 0,
|
dependencies: dependencies
|
||||||
name: name.to_string(),
|
.iter()
|
||||||
unit_type,
|
.map(|handle| AssocActionDependency {
|
||||||
variables: amounts.into_iter().map(|value| AssocValueParamVariable { value, handle: Handle::NULL }).collect(),
|
is_owned: true,
|
||||||
controlled_object_dependency: if controls { written } else { Handle::NULL },
|
dependency: *handle,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
})));
|
owned_parameters: owned,
|
||||||
objects.push((body, action, dxf, class, AssociativeData::SurfaceActionBody(AssocSurfaceActionBody {
|
values: values
|
||||||
kind,
|
.into_iter()
|
||||||
action_body: AssocActionBody { version: 2 },
|
.map(|(name, unit_type, amounts, controls)| AssocValueParam {
|
||||||
surface_body: AssocSurfaceBody { version: 0, dependency: written, is_semi_associative: false, marker: 1, is_semi_override: false, grip_status: 0 },
|
class_version: 0,
|
||||||
..Default::default()
|
name: name.to_string(),
|
||||||
})));
|
unit_type,
|
||||||
|
variables: amounts
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| AssocValueParamVariable {
|
||||||
|
value,
|
||||||
|
handle: Handle::NULL,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
controlled_object_dependency: if controls { written } else { Handle::NULL },
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
objects.push((
|
||||||
|
body,
|
||||||
|
action,
|
||||||
|
dxf,
|
||||||
|
class,
|
||||||
|
AssociativeData::SurfaceActionBody(AssocSurfaceActionBody {
|
||||||
|
kind,
|
||||||
|
action_body: AssocActionBody { version: 2 },
|
||||||
|
surface_body: AssocSurfaceBody {
|
||||||
|
version: 0,
|
||||||
|
dependency: written,
|
||||||
|
is_semi_associative: false,
|
||||||
|
marker: 1,
|
||||||
|
is_semi_override: false,
|
||||||
|
grip_status: 0,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
));
|
||||||
for (handle, owner, dxf, class, data) in objects {
|
for (handle, owner, dxf, class, data) in objects {
|
||||||
allocator.insert_associative_at(handle, owner, dxf, class, data);
|
allocator.insert_associative_at(handle, owner, dxf, class, data);
|
||||||
}
|
}
|
||||||
@@ -3042,15 +3262,18 @@ fn materialize_source_surface_actions(
|
|||||||
let document = &mut *allocator.document;
|
let document = &mut *allocator.document;
|
||||||
if let Some(entity) = document.get_entity_mut(target) {
|
if let Some(entity) = document.get_entity_mut(target) {
|
||||||
entity.common_mut().reactors.push(reactor);
|
entity.common_mut().reactors.push(reactor);
|
||||||
} else if let Some(ObjectType::Associative(object)) = document.objects.get_mut(&target) {
|
} else if let Some(ObjectType::Associative(object)) = document.objects.get_mut(&target)
|
||||||
|
{
|
||||||
object.reactors.push(reactor);
|
object.reactors.push(reactor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
added.push(action);
|
added.push(action);
|
||||||
}
|
}
|
||||||
for (offset, handle) in added.iter().copied().enumerate() {
|
for (offset, handle) in added.iter().copied().enumerate() {
|
||||||
if let Some(ObjectType::Associative(AssociativeObject { data: AssociativeData::Action(action), .. })) =
|
if let Some(ObjectType::Associative(AssociativeObject {
|
||||||
allocator.document.objects.get_mut(&handle)
|
data: AssociativeData::Action(action),
|
||||||
|
..
|
||||||
|
})) = allocator.document.objects.get_mut(&handle)
|
||||||
{
|
{
|
||||||
action.action_index = first_index + offset as i32 + 1;
|
action.action_index = first_index + offset as i32 + 1;
|
||||||
}
|
}
|
||||||
@@ -3120,7 +3343,8 @@ fn materialize_swept_surface_actions(
|
|||||||
};
|
};
|
||||||
let document = &mut *allocator.document;
|
let document = &mut *allocator.document;
|
||||||
let mut next = || document.allocate_handle();
|
let mut next = || document.allocate_handle();
|
||||||
let (action, body, written, profile_param, path_param) = (next(), next(), next(), next(), next());
|
let (action, body, written, profile_param, path_param) =
|
||||||
|
(next(), next(), next(), next(), next());
|
||||||
let dependency = |dependent_on: Handle, read: bool, id: i32, order: i32| AssocDependency {
|
let dependency = |dependent_on: Handle, read: bool, id: i32, order: i32| AssocDependency {
|
||||||
class_version: 2,
|
class_version: 2,
|
||||||
status: 0,
|
status: 0,
|
||||||
@@ -3136,15 +3360,34 @@ fn materialize_swept_surface_actions(
|
|||||||
dependency_body: Handle::NULL,
|
dependency_body: Handle::NULL,
|
||||||
dependency_body_id: id,
|
dependency_body_id: id,
|
||||||
};
|
};
|
||||||
let mut objects: Vec<(Handle, Handle, &'static str, &'static str, AssociativeData)> = Vec::new();
|
let mut objects: Vec<(Handle, Handle, &'static str, &'static str, AssociativeData)> =
|
||||||
|
Vec::new();
|
||||||
// The profile (a region by its edges) and the path, read in that order.
|
// The profile (a region by its edges) and the path, read in that order.
|
||||||
let Some(profile_reads) = source_reads(document, action, profile_param, profile, 2, &mut objects) else { continue };
|
let Some(profile_reads) =
|
||||||
let Some(path_reads) = source_reads(document, action, path_param, path, profile_reads.len() as i32 + 2, &mut objects) else { continue };
|
source_reads(document, action, profile_param, profile, 2, &mut objects)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(path_reads) = source_reads(
|
||||||
|
document,
|
||||||
|
action,
|
||||||
|
path_param,
|
||||||
|
path,
|
||||||
|
profile_reads.len() as i32 + 2,
|
||||||
|
&mut objects,
|
||||||
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let mut reactors: Vec<(Handle, Handle)> = vec![(surface, written)];
|
let mut reactors: Vec<(Handle, Handle)> = vec![(surface, written)];
|
||||||
reactors.extend(profile_reads.iter().map(|(_, read)| (profile, *read)));
|
reactors.extend(profile_reads.iter().map(|(_, read)| (profile, *read)));
|
||||||
reactors.extend(path_reads.iter().map(|(_, read)| (path, *read)));
|
reactors.extend(path_reads.iter().map(|(_, read)| (path, *read)));
|
||||||
let mut dependencies = vec![written];
|
let mut dependencies = vec![written];
|
||||||
dependencies.extend(profile_reads.iter().chain(&path_reads).map(|(_, read)| *read));
|
dependencies.extend(
|
||||||
|
profile_reads
|
||||||
|
.iter()
|
||||||
|
.chain(&path_reads)
|
||||||
|
.map(|(_, read)| *read),
|
||||||
|
);
|
||||||
let mut value_handles = [Handle::NULL, Handle::NULL];
|
let mut value_handles = [Handle::NULL, Handle::NULL];
|
||||||
let mut network_actions = vec![action];
|
let mut network_actions = vec![action];
|
||||||
// Scale (slot 0) and twist (slot 1) expressions.
|
// Scale (slot 0) and twist (slot 1) expressions.
|
||||||
@@ -3281,22 +3524,51 @@ fn materialize_swept_surface_actions(
|
|||||||
grip_status: 0,
|
grip_status: 0,
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})));
|
}),
|
||||||
objects.push((written, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", AssociativeData::Dependency(dependency(surface, false, 1, i32::MIN))));
|
));
|
||||||
let path_parameter = |name: &str, children: Vec<Handle>| AssociativeData::PathActionParam(AssocPathActionParam {
|
objects.push((
|
||||||
compound: AssocCompoundActionParam {
|
written,
|
||||||
action_param: AssocActionParam { is_r2013: 1, version: 0, name: name.to_string() },
|
action,
|
||||||
class_version: 0,
|
"ACDBASSOCDEPENDENCY",
|
||||||
status: 0,
|
"AcDbAssocDependency",
|
||||||
parameters: children,
|
AssociativeData::Dependency(dependency(surface, false, 1, i32::MIN)),
|
||||||
child_parameter: None,
|
));
|
||||||
},
|
let path_parameter = |name: &str, children: Vec<Handle>| {
|
||||||
version: 0,
|
AssociativeData::PathActionParam(AssocPathActionParam {
|
||||||
});
|
compound: AssocCompoundActionParam {
|
||||||
objects.push((profile_param, action, "ACDBASSOCPATHACTIONPARAM", "AcDbAssocPathActionParam",
|
action_param: AssocActionParam {
|
||||||
path_parameter("SweepProfile", profile_reads.iter().map(|(edge, _)| *edge).collect())));
|
is_r2013: 1,
|
||||||
objects.push((path_param, action, "ACDBASSOCPATHACTIONPARAM", "AcDbAssocPathActionParam",
|
version: 0,
|
||||||
path_parameter("SweepPath", path_reads.iter().map(|(edge, _)| *edge).collect())));
|
name: name.to_string(),
|
||||||
|
},
|
||||||
|
class_version: 0,
|
||||||
|
status: 0,
|
||||||
|
parameters: children,
|
||||||
|
child_parameter: None,
|
||||||
|
},
|
||||||
|
version: 0,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
objects.push((
|
||||||
|
profile_param,
|
||||||
|
action,
|
||||||
|
"ACDBASSOCPATHACTIONPARAM",
|
||||||
|
"AcDbAssocPathActionParam",
|
||||||
|
path_parameter(
|
||||||
|
"SweepProfile",
|
||||||
|
profile_reads.iter().map(|(edge, _)| *edge).collect(),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
objects.push((
|
||||||
|
path_param,
|
||||||
|
action,
|
||||||
|
"ACDBASSOCPATHACTIONPARAM",
|
||||||
|
"AcDbAssocPathActionParam",
|
||||||
|
path_parameter(
|
||||||
|
"SweepPath",
|
||||||
|
path_reads.iter().map(|(edge, _)| *edge).collect(),
|
||||||
|
),
|
||||||
|
));
|
||||||
for (handle, owner, dxf, class, data) in objects {
|
for (handle, owner, dxf, class, data) in objects {
|
||||||
allocator.insert_associative_at(handle, owner, dxf, class, data);
|
allocator.insert_associative_at(handle, owner, dxf, class, data);
|
||||||
}
|
}
|
||||||
@@ -3658,13 +3930,29 @@ fn materialize_scope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
refresh_foreign_values(allocator.document, &foreign_actions, parameters);
|
refresh_foreign_values(allocator.document, &foreign_actions, parameters);
|
||||||
let pending = if materialize_all_parameters { pending_swept_surfaces(allocator.document) } else { Vec::new() };
|
let pending = if materialize_all_parameters {
|
||||||
|
pending_swept_surfaces(allocator.document)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
let mut surface_actions = materialize_swept_surface_actions(
|
let mut surface_actions = materialize_swept_surface_actions(
|
||||||
&mut allocator, network_handle, parameters, &pending, first_foreign + foreign_actions.len() as i32);
|
&mut allocator,
|
||||||
let pending_sources = if materialize_all_parameters { pending_source_surfaces(allocator.document) } else { Vec::new() };
|
network_handle,
|
||||||
|
parameters,
|
||||||
|
&pending,
|
||||||
|
first_foreign + foreign_actions.len() as i32,
|
||||||
|
);
|
||||||
|
let pending_sources = if materialize_all_parameters {
|
||||||
|
pending_source_surfaces(allocator.document)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
let source_actions = materialize_source_surface_actions(
|
let source_actions = materialize_source_surface_actions(
|
||||||
&mut allocator, network_handle, &pending_sources,
|
&mut allocator,
|
||||||
first_foreign + (foreign_actions.len() + surface_actions.len()) as i32);
|
network_handle,
|
||||||
|
&pending_sources,
|
||||||
|
first_foreign + (foreign_actions.len() + surface_actions.len()) as i32,
|
||||||
|
);
|
||||||
surface_actions.extend(source_actions);
|
surface_actions.extend(source_actions);
|
||||||
|
|
||||||
let network = AssocNetwork {
|
let network = AssocNetwork {
|
||||||
|
|||||||
+31
-17
@@ -7,12 +7,10 @@
|
|||||||
// cache — block-definition and property caches
|
// cache — block-definition and property caches
|
||||||
pub mod annotative;
|
pub mod annotative;
|
||||||
pub mod assoc_array;
|
pub mod assoc_array;
|
||||||
pub mod block_visibility;
|
|
||||||
pub mod compare;
|
|
||||||
pub mod section_cull;
|
|
||||||
pub mod sub_select;
|
|
||||||
pub mod block_service;
|
pub mod block_service;
|
||||||
|
pub mod block_visibility;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
|
pub mod compare;
|
||||||
pub mod convert;
|
pub mod convert;
|
||||||
pub mod creation_style;
|
pub mod creation_style;
|
||||||
pub(crate) mod frame;
|
pub(crate) mod frame;
|
||||||
@@ -20,6 +18,8 @@ pub mod model;
|
|||||||
pub mod pick;
|
pub mod pick;
|
||||||
pub mod pipeline;
|
pub mod pipeline;
|
||||||
pub(crate) mod render_graph;
|
pub(crate) mod render_graph;
|
||||||
|
pub mod section_cull;
|
||||||
|
pub mod sub_select;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
pub mod view;
|
pub mod view;
|
||||||
|
|
||||||
@@ -4867,10 +4867,14 @@ impl Scene {
|
|||||||
/// Fit a layout's sheet viewport to its page. (Taking the sheet out
|
/// Fit a layout's sheet viewport to its page. (Taking the sheet out
|
||||||
/// instead would leave a viewport onto the model looking like the sheet.)
|
/// instead would leave a viewport onto the model looking like the sheet.)
|
||||||
pub fn fit_layout_sheet(&mut self, layout_name: &str) {
|
pub fn fit_layout_sheet(&mut self, layout_name: &str) {
|
||||||
let layout = self.document.objects.values().find_map(|object| match object {
|
let layout = self
|
||||||
ObjectType::Layout(layout) if layout.name == layout_name => Some(layout.clone()),
|
.document
|
||||||
_ => None,
|
.objects
|
||||||
});
|
.values()
|
||||||
|
.find_map(|object| match object {
|
||||||
|
ObjectType::Layout(layout) if layout.name == layout_name => Some(layout.clone()),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
let Some(layout) = layout else {
|
let Some(layout) = layout else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -7422,8 +7426,8 @@ impl Scene {
|
|||||||
let t_patch = iced::time::Instant::now();
|
let t_patch = iced::time::Instant::now();
|
||||||
// A patch whose set will not equal the memo's runs leaves the memo
|
// A patch whose set will not equal the memo's runs leaves the memo
|
||||||
// holding its own copies, taken while the set is still whole.
|
// holding its own copies, taken while the set is still whole.
|
||||||
let shareable = self.uses_resident_memo(block, frozen_layers)
|
let shareable =
|
||||||
&& self.resident_runs_match_memo(block);
|
self.uses_resident_memo(block, frozen_layers) && self.resident_runs_match_memo(block);
|
||||||
if self.uses_resident_memo(block, frozen_layers) && !shareable {
|
if self.uses_resident_memo(block, frozen_layers) && !shareable {
|
||||||
self.detach_memo_backing();
|
self.detach_memo_backing();
|
||||||
}
|
}
|
||||||
@@ -7966,8 +7970,9 @@ impl Scene {
|
|||||||
apply_mode(&mut wires, mode);
|
apply_mode(&mut wires, mode);
|
||||||
model_wires.extend(wires);
|
model_wires.extend(wires);
|
||||||
}
|
}
|
||||||
let paper = crate::scene::model::instance_model::expand_stubs(&self.paper_sheet_wires_arc())
|
let paper =
|
||||||
.into_owned();
|
crate::scene::model::instance_model::expand_stubs(&self.paper_sheet_wires_arc())
|
||||||
|
.into_owned();
|
||||||
(paper, model_wires)
|
(paper, model_wires)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10653,7 +10658,7 @@ impl Scene {
|
|||||||
let clip = view_rot * (cursor - eye).as_vec3().extend(1.0);
|
let clip = view_rot * (cursor - eye).as_vec3().extend(1.0);
|
||||||
if !clip.is_finite() || clip.w <= 1e-6 {
|
if !clip.is_finite() || clip.w <= 1e-6 {
|
||||||
return crate::scene::pick::interaction_index::InteractionCandidates::all(wires)
|
return crate::scene::pick::interaction_index::InteractionCandidates::all(wires)
|
||||||
.materialize_stubs();
|
.materialize_stubs();
|
||||||
}
|
}
|
||||||
let ndc = clip.truncate() / clip.w;
|
let ndc = clip.truncate() / clip.w;
|
||||||
let screen = iced::Point::new(
|
let screen = iced::Point::new(
|
||||||
@@ -13204,13 +13209,21 @@ pub(crate) fn drop_layout_viewports(document: &mut codec::CadDocument, block: Ha
|
|||||||
for handle in &viewports {
|
for handle in &viewports {
|
||||||
document.remove_entity(*handle);
|
document.remove_entity(*handle);
|
||||||
}
|
}
|
||||||
if let Some(record) = document.block_records.iter_mut().find(|record| record.handle == block) {
|
if let Some(record) = document
|
||||||
record.entity_handles.retain(|handle| !viewports.contains(handle));
|
.block_records
|
||||||
|
.iter_mut()
|
||||||
|
.find(|record| record.handle == block)
|
||||||
|
{
|
||||||
|
record
|
||||||
|
.entity_handles
|
||||||
|
.retain(|handle| !viewports.contains(handle));
|
||||||
}
|
}
|
||||||
for object in document.objects.values_mut() {
|
for object in document.objects.values_mut() {
|
||||||
if let ObjectType::Layout(layout) = object {
|
if let ObjectType::Layout(layout) = object {
|
||||||
if layout.block_record == block {
|
if layout.block_record == block {
|
||||||
layout.viewports.retain(|handle| !viewports.contains(handle));
|
layout
|
||||||
|
.viewports
|
||||||
|
.retain(|handle| !viewports.contains(handle));
|
||||||
if viewports.contains(&layout.viewport) {
|
if viewports.contains(&layout.viewport) {
|
||||||
layout.viewport = Handle::NULL;
|
layout.viewport = Handle::NULL;
|
||||||
}
|
}
|
||||||
@@ -13220,7 +13233,8 @@ pub(crate) fn drop_layout_viewports(document: &mut codec::CadDocument, block: Ha
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// LAYOUTCREATEVIEWPORT: a layout opened for the first time gets a viewport.
|
/// LAYOUTCREATEVIEWPORT: a layout opened for the first time gets a viewport.
|
||||||
static LAYOUT_CREATE_VIEWPORT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
|
static LAYOUT_CREATE_VIEWPORT: std::sync::atomic::AtomicBool =
|
||||||
|
std::sync::atomic::AtomicBool::new(true);
|
||||||
|
|
||||||
pub fn layout_create_viewport() -> bool {
|
pub fn layout_create_viewport() -> bool {
|
||||||
LAYOUT_CREATE_VIEWPORT.load(std::sync::atomic::Ordering::Relaxed)
|
LAYOUT_CREATE_VIEWPORT.load(std::sync::atomic::Ordering::Relaxed)
|
||||||
|
|||||||
+108
-38
@@ -673,12 +673,24 @@ pub(crate) fn brightness_contrast_lut(brightness: u8, contrast: u8) -> [u8; 256]
|
|||||||
let mut x = i as f64 / 255.0;
|
let mut x = i as f64 / 255.0;
|
||||||
x = match brightness.min(100) {
|
x = match brightness.min(100) {
|
||||||
0 => 0.0,
|
0 => 0.0,
|
||||||
100 => if i == 0 { 0.0 } else { 1.0 },
|
100 => {
|
||||||
|
if i == 0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
b => x.powf(curve(b)),
|
b => x.powf(curve(b)),
|
||||||
};
|
};
|
||||||
x = match contrast.min(100) {
|
x = match contrast.min(100) {
|
||||||
0 => 0.5,
|
0 => 0.5,
|
||||||
100 => if x >= 0.5 { 1.0 } else { 0.0 },
|
100 => {
|
||||||
|
if x >= 0.5 {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
c => {
|
c => {
|
||||||
let d = 2.0 * x - 1.0;
|
let d = 2.0 * x - 1.0;
|
||||||
0.5 + 0.5 * d.signum() * d.abs().powf(curve(c))
|
0.5 + 0.5 * d.signum() * d.abs().powf(curve(c))
|
||||||
@@ -705,7 +717,12 @@ fn adjusted_image_pixels(
|
|||||||
type Key = (String, usize, u8, u8);
|
type Key = (String, usize, u8, u8);
|
||||||
type Entry = (Arc<Vec<u8>>, Arc<Vec<u8>>);
|
type Entry = (Arc<Vec<u8>>, Arc<Vec<u8>>);
|
||||||
static CACHE: OnceLock<Mutex<HashMap<Key, Entry>>> = OnceLock::new();
|
static CACHE: OnceLock<Mutex<HashMap<Key, Entry>>> = OnceLock::new();
|
||||||
let key = (path.to_string(), Arc::as_ptr(pixels) as usize, brightness, contrast);
|
let key = (
|
||||||
|
path.to_string(),
|
||||||
|
Arc::as_ptr(pixels) as usize,
|
||||||
|
brightness,
|
||||||
|
contrast,
|
||||||
|
);
|
||||||
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
||||||
if let Some((_, hit)) = cache.lock().unwrap_or_else(|p| p.into_inner()).get(&key) {
|
if let Some((_, hit)) = cache.lock().unwrap_or_else(|p| p.into_inner()).get(&key) {
|
||||||
return Arc::clone(hit);
|
return Arc::clone(hit);
|
||||||
@@ -848,7 +865,10 @@ struct NativeFootprint {
|
|||||||
codec_factor: u64,
|
codec_factor: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn native_footprint(format: Option<image::ImageFormat>, color: Option<image::ColorType>) -> NativeFootprint {
|
fn native_footprint(
|
||||||
|
format: Option<image::ImageFormat>,
|
||||||
|
color: Option<image::ColorType>,
|
||||||
|
) -> NativeFootprint {
|
||||||
let native_bpp = color.map_or(4u64, |color| match color {
|
let native_bpp = color.map_or(4u64, |color| match color {
|
||||||
image::ColorType::L8 => 1,
|
image::ColorType::L8 => 1,
|
||||||
image::ColorType::La8 => 2,
|
image::ColorType::La8 => 2,
|
||||||
@@ -881,19 +901,17 @@ fn native_footprint(format: Option<image::ImageFormat>, color: Option<image::Col
|
|||||||
// chunk scratch (a TOTAL cap), so no multiplier is needed: the honest
|
// chunk scratch (a TOTAL cap), so no multiplier is needed: the honest
|
||||||
// peak is the coexisting native+RGBA pair, already in peak_bpp.
|
// peak is the coexisting native+RGBA pair, already in peak_bpp.
|
||||||
let codec_factor = 1;
|
let codec_factor = 1;
|
||||||
NativeFootprint { peak_bpp, codec_factor }
|
NativeFootprint {
|
||||||
|
peak_bpp,
|
||||||
|
codec_factor,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Budget decision for probed pixel dimensions. Pure and cfg-independent
|
/// Budget decision for probed pixel dimensions. Pure and cfg-independent
|
||||||
/// when the budgets are passed explicitly, so tests can probe arbitrary
|
/// when the budgets are passed explicitly, so tests can probe arbitrary
|
||||||
/// budgets; the decode path always passes this target's constants.
|
/// budgets; the decode path always passes this target's constants.
|
||||||
#[cfg_attr(not(test), allow(dead_code))]
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
fn tier_for(
|
fn tier_for(width: u32, height: u32, max_decode_bytes: u64, max_display_pixels: u64) -> DecodeTier {
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
max_decode_bytes: u64,
|
|
||||||
max_display_pixels: u64,
|
|
||||||
) -> DecodeTier {
|
|
||||||
tier_for_native(width, height, 4, 1, max_decode_bytes, max_display_pixels)
|
tier_for_native(width, height, 4, 1, max_decode_bytes, max_display_pixels)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -909,11 +927,7 @@ fn tier_for_native(
|
|||||||
max_display_pixels: u64,
|
max_display_pixels: u64,
|
||||||
) -> DecodeTier {
|
) -> DecodeTier {
|
||||||
let pixels = width as u64 * height as u64;
|
let pixels = width as u64 * height as u64;
|
||||||
if pixels
|
if pixels.saturating_mul(peak_bpp).saturating_mul(codec_factor) > max_decode_bytes {
|
||||||
.saturating_mul(peak_bpp)
|
|
||||||
.saturating_mul(codec_factor)
|
|
||||||
> max_decode_bytes
|
|
||||||
{
|
|
||||||
return DecodeTier::OverBudget;
|
return DecodeTier::OverBudget;
|
||||||
}
|
}
|
||||||
if pixels <= max_display_pixels {
|
if pixels <= max_display_pixels {
|
||||||
@@ -925,7 +939,11 @@ fn tier_for_native(
|
|||||||
let (target_w, target_h) = downsample_to_cap(width, height, max_display_pixels);
|
let (target_w, target_h) = downsample_to_cap(width, height, max_display_pixels);
|
||||||
let resize_peak = pixels
|
let resize_peak = pixels
|
||||||
.saturating_mul(4)
|
.saturating_mul(4)
|
||||||
.saturating_add((width as u64).saturating_mul(target_h as u64).saturating_mul(16))
|
.saturating_add(
|
||||||
|
(width as u64)
|
||||||
|
.saturating_mul(target_h as u64)
|
||||||
|
.saturating_mul(16),
|
||||||
|
)
|
||||||
.saturating_add(target_w as u64 * target_h as u64 * 4);
|
.saturating_add(target_w as u64 * target_h as u64 * 4);
|
||||||
if resize_peak > max_decode_bytes {
|
if resize_peak > max_decode_bytes {
|
||||||
DecodeTier::OverBudget
|
DecodeTier::OverBudget
|
||||||
@@ -1037,7 +1055,10 @@ fn decode_with_budgets<R: std::io::BufRead + std::io::Seek>(
|
|||||||
limits.max_alloc = Some(match reader.format() {
|
limits.max_alloc = Some(match reader.format() {
|
||||||
Some(image::ImageFormat::Gif) => probed
|
Some(image::ImageFormat::Gif) => probed
|
||||||
.map(|(width, height, _, _)| {
|
.map(|(width, height, _, _)| {
|
||||||
(width as u64).saturating_mul(height as u64).saturating_mul(4) + 64 * 1024 * 1024
|
(width as u64)
|
||||||
|
.saturating_mul(height as u64)
|
||||||
|
.saturating_mul(4)
|
||||||
|
+ 64 * 1024 * 1024
|
||||||
})
|
})
|
||||||
.unwrap_or(max_decode_bytes)
|
.unwrap_or(max_decode_bytes)
|
||||||
.min(max_decode_bytes),
|
.min(max_decode_bytes),
|
||||||
@@ -1067,7 +1088,12 @@ fn decode_with_budgets<R: std::io::BufRead + std::io::Seek>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let rgba = if target_w != width || target_h != height {
|
let rgba = if target_w != width || target_h != height {
|
||||||
image::imageops::resize(&rgba, target_w, target_h, image::imageops::FilterType::Lanczos3)
|
image::imageops::resize(
|
||||||
|
&rgba,
|
||||||
|
target_w,
|
||||||
|
target_h,
|
||||||
|
image::imageops::FilterType::Lanczos3,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
rgba
|
rgba
|
||||||
};
|
};
|
||||||
@@ -1123,7 +1149,10 @@ fn refusal_message(path: &str, width: u32, height: u32) -> String {
|
|||||||
)
|
)
|
||||||
.into_owned();
|
.into_owned();
|
||||||
}
|
}
|
||||||
let decoded_mb = (width as u64).saturating_mul(height as u64).saturating_mul(4) / (1024 * 1024);
|
let decoded_mb = (width as u64)
|
||||||
|
.saturating_mul(height as u64)
|
||||||
|
.saturating_mul(4)
|
||||||
|
/ (1024 * 1024);
|
||||||
let limit_mb = MAX_DECODE_BYTES / (1024 * 1024);
|
let limit_mb = MAX_DECODE_BYTES / (1024 * 1024);
|
||||||
crate::tf!(
|
crate::tf!(
|
||||||
"\"{name}\" is {width}×{height} px and would decode to {decoded} MB, over the {limit} MB image limit of this build — it cannot be attached. Images within the limit are attached at a reduced display resolution when needed.",
|
"\"{name}\" is {width}×{height} px and would decode to {decoded} MB, over the {limit} MB image limit of this build — it cannot be attached. Images within the limit are attached at a reduced display resolution when needed.",
|
||||||
@@ -1156,7 +1185,9 @@ pub fn decode_refusals() -> Vec<String> {
|
|||||||
/// Take and clear — for the drains that own every queued note (the poll and
|
/// Take and clear — for the drains that own every queued note (the poll and
|
||||||
/// the open-settle paths).
|
/// the open-settle paths).
|
||||||
pub fn take_decode_refusals() -> Vec<String> {
|
pub fn take_decode_refusals() -> Vec<String> {
|
||||||
let mut notes = refusal_notes().lock().unwrap_or_else(|error| error.into_inner());
|
let mut notes = refusal_notes()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|error| error.into_inner());
|
||||||
std::mem::take(&mut *notes)
|
std::mem::take(&mut *notes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1170,7 +1201,9 @@ fn decode_or_note(attempt: DecodeAttempt, path: &str) -> Option<DecodedImage> {
|
|||||||
// one note per distinct message so the command line is not
|
// one note per distinct message so the command line is not
|
||||||
// spammed by a drawing that inserts it repeatedly.
|
// spammed by a drawing that inserts it repeatedly.
|
||||||
let message = refusal_message(path, width, height);
|
let message = refusal_message(path, width, height);
|
||||||
let mut notes = refusal_notes().lock().unwrap_or_else(|error| error.into_inner());
|
let mut notes = refusal_notes()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|error| error.into_inner());
|
||||||
if notes.last() != Some(&message) {
|
if notes.last() != Some(&message) {
|
||||||
notes.push(message);
|
notes.push(message);
|
||||||
}
|
}
|
||||||
@@ -1338,7 +1371,9 @@ mod web_fetch {
|
|||||||
// No stream API (or already consumed): fall back to buffering,
|
// No stream API (or already consumed): fall back to buffering,
|
||||||
// with the received-length check still applying after.
|
// with the received-length check still applying after.
|
||||||
let buffer = JsFuture::from(
|
let buffer = JsFuture::from(
|
||||||
response.array_buffer().map_err(|_| RemoteFetchError::Network)?,
|
response
|
||||||
|
.array_buffer()
|
||||||
|
.map_err(|_| RemoteFetchError::Network)?,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| rejection_kind(&error))?;
|
.map_err(|error| rejection_kind(&error))?;
|
||||||
@@ -1348,7 +1383,9 @@ mod web_fetch {
|
|||||||
}
|
}
|
||||||
return Ok(bytes);
|
return Ok(bytes);
|
||||||
};
|
};
|
||||||
let reader = stream.get_reader().dyn_into::<web_sys::ReadableStreamDefaultReader>()
|
let reader = stream
|
||||||
|
.get_reader()
|
||||||
|
.dyn_into::<web_sys::ReadableStreamDefaultReader>()
|
||||||
.map_err(|_| RemoteFetchError::Network)?;
|
.map_err(|_| RemoteFetchError::Network)?;
|
||||||
let mut bytes: Vec<u8> = Vec::new();
|
let mut bytes: Vec<u8> = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
@@ -1387,8 +1424,7 @@ mod web_fetch {
|
|||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
|
||||||
let window = web_sys::window().ok_or(RemoteFetchError::Network)?;
|
let window = web_sys::window().ok_or(RemoteFetchError::Network)?;
|
||||||
let controller =
|
let controller = web_sys::AbortController::new().map_err(|_| RemoteFetchError::Network)?;
|
||||||
web_sys::AbortController::new().map_err(|_| RemoteFetchError::Network)?;
|
|
||||||
let init = web_sys::RequestInit::new();
|
let init = web_sys::RequestInit::new();
|
||||||
init.set_method("GET");
|
init.set_method("GET");
|
||||||
init.set_signal(Some(&controller.signal()));
|
init.set_signal(Some(&controller.signal()));
|
||||||
@@ -1470,8 +1506,8 @@ fn fetch_remote(url: &str) -> Option<Vec<u8>> {
|
|||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub use web_fetch::{
|
pub use web_fetch::{
|
||||||
take_outcome as take_remote_fetch_outcome, take_overflowed,
|
take_outcome as take_remote_fetch_outcome, take_overflowed, RemoteFetchError,
|
||||||
RemoteFetchError, RemoteFetchOutcome,
|
RemoteFetchOutcome,
|
||||||
};
|
};
|
||||||
// The capped streaming reader itself, shared with the `?file=` deep-link
|
// The capped streaming reader itself, shared with the `?file=` deep-link
|
||||||
// fetch (`io::web_link`) so both doors enforce the same 64 MB / 8 s bounds.
|
// fetch (`io::web_link`) so both doors enforce the same 64 MB / 8 s bounds.
|
||||||
@@ -1510,7 +1546,10 @@ mod tests {
|
|||||||
"{color:?}"
|
"{color:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert_eq!(native_footprint(Some(gif), Some(ColorType::Rgba8)).peak_bpp, 8);
|
assert_eq!(
|
||||||
|
native_footprint(Some(gif), Some(ColorType::Rgba8)).peak_bpp,
|
||||||
|
8
|
||||||
|
);
|
||||||
assert_eq!(native_footprint(None, None).peak_bpp, 8);
|
assert_eq!(native_footprint(None, None).peak_bpp, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1614,12 +1653,21 @@ mod tests {
|
|||||||
assert_eq!(tier_for(64, 64, 1_000_000, 10_000), DecodeTier::Full);
|
assert_eq!(tier_for(64, 64, 1_000_000, 10_000), DecodeTier::Full);
|
||||||
// Pixels over the display cap while RGBA stays within the decode
|
// Pixels over the display cap while RGBA stays within the decode
|
||||||
// budget: decode, then downsample before storing.
|
// budget: decode, then downsample before storing.
|
||||||
assert_eq!(tier_for(400, 400, 2_000_000, 10_000), DecodeTier::Downsample);
|
assert_eq!(
|
||||||
|
tier_for(400, 400, 2_000_000, 10_000),
|
||||||
|
DecodeTier::Downsample
|
||||||
|
);
|
||||||
// …unless the resize transient (decoded + f32 intermediate + output)
|
// …unless the resize transient (decoded + f32 intermediate + output)
|
||||||
// is what overflows the budget.
|
// is what overflows the budget.
|
||||||
assert_eq!(tier_for(400, 400, 1_000_000, 10_000), DecodeTier::OverBudget);
|
assert_eq!(
|
||||||
|
tier_for(400, 400, 1_000_000, 10_000),
|
||||||
|
DecodeTier::OverBudget
|
||||||
|
);
|
||||||
// RGBA over the decode budget.
|
// RGBA over the decode budget.
|
||||||
assert_eq!(tier_for(2_000, 2_000, 1_000_000, 10_000), DecodeTier::OverBudget);
|
assert_eq!(
|
||||||
|
tier_for(2_000, 2_000, 1_000_000, 10_000),
|
||||||
|
DecodeTier::OverBudget
|
||||||
|
);
|
||||||
// Exact boundary counts stay in the lower tier (limits are ">").
|
// Exact boundary counts stay in the lower tier (limits are ">").
|
||||||
assert_eq!(tier_for(50, 200, 1_000_000, 10_000), DecodeTier::Full);
|
assert_eq!(tier_for(50, 200, 1_000_000, 10_000), DecodeTier::Full);
|
||||||
assert_eq!(tier_for(500, 500, 1_000_000, 1_000_000), DecodeTier::Full);
|
assert_eq!(tier_for(500, 500, 1_000_000, 1_000_000), DecodeTier::Full);
|
||||||
@@ -1653,10 +1701,18 @@ mod tests {
|
|||||||
// Over the cap: the product fits while both sides exceed 1 px, and
|
// Over the cap: the product fits while both sides exceed 1 px, and
|
||||||
// the aspect survives to within rounding (unless a side is too
|
// the aspect survives to within rounding (unless a side is too
|
||||||
// small to express it — a 3 px side clamps to 1 px by design).
|
// small to express it — a 3 px side clamps to 1 px by design).
|
||||||
for (width, height) in [(4097u32, 4096u32), (20000, 15000), (12345, 6700), (3, 20000000)] {
|
for (width, height) in [
|
||||||
|
(4097u32, 4096u32),
|
||||||
|
(20000, 15000),
|
||||||
|
(12345, 6700),
|
||||||
|
(3, 20000000),
|
||||||
|
] {
|
||||||
let cap = 4096u64 * 4096;
|
let cap = 4096u64 * 4096;
|
||||||
let (w, h) = downsample_to_cap(width, height, cap);
|
let (w, h) = downsample_to_cap(width, height, cap);
|
||||||
assert!((w as u64) * h as u64 <= cap, "{width}×{height} -> {w}×{h} over cap");
|
assert!(
|
||||||
|
(w as u64) * h as u64 <= cap,
|
||||||
|
"{width}×{height} -> {w}×{h} over cap"
|
||||||
|
);
|
||||||
assert!(w >= 1 && h >= 1);
|
assert!(w >= 1 && h >= 1);
|
||||||
if w >= 8 && h >= 8 {
|
if w >= 8 && h >= 8 {
|
||||||
let aspect = width as f64 / height as f64;
|
let aspect = width as f64 / height as f64;
|
||||||
@@ -1720,13 +1776,27 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn refusals_are_noted_once_per_message() {
|
fn refusals_are_noted_once_per_message() {
|
||||||
let _ = take_decode_refusals();
|
let _ = take_decode_refusals();
|
||||||
let refuse = || DecodeAttempt::Refused { width: 30000, height: 30000 };
|
let refuse = || DecodeAttempt::Refused {
|
||||||
|
width: 30000,
|
||||||
|
height: 30000,
|
||||||
|
};
|
||||||
assert!(decode_or_note(refuse(), "C:/gis/aerial.tif").is_none());
|
assert!(decode_or_note(refuse(), "C:/gis/aerial.tif").is_none());
|
||||||
assert!(decode_or_note(refuse(), "C:/gis/aerial.tif").is_none());
|
assert!(decode_or_note(refuse(), "C:/gis/aerial.tif").is_none());
|
||||||
let notes = take_decode_refusals();
|
let notes = take_decode_refusals();
|
||||||
assert_eq!(notes.len(), 1, "duplicate messages collapse: {notes:?}");
|
assert_eq!(notes.len(), 1, "duplicate messages collapse: {notes:?}");
|
||||||
assert!(notes[0].contains("30000×30000"), "dimensions in: {}", notes[0]);
|
assert!(
|
||||||
assert!(notes[0].contains("aerial.tif"), "file name in: {}", notes[0]);
|
notes[0].contains("30000×30000"),
|
||||||
assert!(take_decode_refusals().is_empty(), "the drain cleared the queue");
|
"dimensions in: {}",
|
||||||
|
notes[0]
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
notes[0].contains("aerial.tif"),
|
||||||
|
"file name in: {}",
|
||||||
|
notes[0]
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
take_decode_refusals().is_empty(),
|
||||||
|
"the drain cleared the queue"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,12 @@ pub fn stubbable(wire: &WireModel) -> bool {
|
|||||||
/// The XY box a prototype's wires cover, before any copy's offset.
|
/// The XY box a prototype's wires cover, before any copy's offset.
|
||||||
pub fn prototype_aabb(prototype: &[WireModel]) -> [f32; 4] {
|
pub fn prototype_aabb(prototype: &[WireModel]) -> [f32; 4] {
|
||||||
prototype.iter().fold(
|
prototype.iter().fold(
|
||||||
[f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY],
|
[
|
||||||
|
f32::INFINITY,
|
||||||
|
f32::INFINITY,
|
||||||
|
f32::NEG_INFINITY,
|
||||||
|
f32::NEG_INFINITY,
|
||||||
|
],
|
||||||
|acc, wire| {
|
|acc, wire| {
|
||||||
[
|
[
|
||||||
acc[0].min(wire.aabb[0]),
|
acc[0].min(wire.aabb[0]),
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ use kernel::brep::Body;
|
|||||||
pub(crate) fn embedded_section(entity: &EntityType) -> Option<EmbeddedEntity> {
|
pub(crate) fn embedded_section(entity: &EntityType) -> Option<EmbeddedEntity> {
|
||||||
match entity {
|
match entity {
|
||||||
// A region as the curve round it, as the reference records it.
|
// A region as the curve round it, as the reference records it.
|
||||||
EntityType::Region(region) => Some(super::sweep_model::region_boundary_entity(entity)
|
EntityType::Region(region) => Some(
|
||||||
.map_or_else(|| EmbeddedEntity::Region(region.clone()), |(curve, _)| curve)),
|
super::sweep_model::region_boundary_entity(entity).map_or_else(
|
||||||
|
|| EmbeddedEntity::Region(region.clone()),
|
||||||
|
|(curve, _)| curve,
|
||||||
|
),
|
||||||
|
),
|
||||||
EntityType::Point(point) => Some(EmbeddedEntity::Point(point.clone())),
|
EntityType::Point(point) => Some(EmbeddedEntity::Point(point.clone())),
|
||||||
_ => embedded_path(entity),
|
_ => embedded_path(entity),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,22 @@ pub const SURFACE_SOURCES_APP: &str = "OCS_SURFACE_SOURCES";
|
|||||||
pub enum SurfaceSources {
|
pub enum SurfaceSources {
|
||||||
/// EXTRUDE: the profile, the height along its normal (or the length of a
|
/// EXTRUDE: the profile, the height along its normal (or the length of a
|
||||||
/// given direction) and the taper angle.
|
/// given direction) and the taper angle.
|
||||||
Extrude { profile: Handle, height: f64, direction: Option<DVec3>, taper: f64 },
|
Extrude {
|
||||||
|
profile: Handle,
|
||||||
|
height: f64,
|
||||||
|
direction: Option<DVec3>,
|
||||||
|
taper: f64,
|
||||||
|
},
|
||||||
/// REVOLVE: the profile, the axis (an axis object or two points), the
|
/// REVOLVE: the profile, the axis (an axis object or two points), the
|
||||||
/// angle and the start angle.
|
/// angle and the start angle.
|
||||||
Revolve { profile: Handle, axis: Option<Handle>, start: DVec3, end: DVec3, angle: f64, start_angle: f64 },
|
Revolve {
|
||||||
|
profile: Handle,
|
||||||
|
axis: Option<Handle>,
|
||||||
|
start: DVec3,
|
||||||
|
end: DVec3,
|
||||||
|
angle: f64,
|
||||||
|
start_angle: f64,
|
||||||
|
},
|
||||||
/// LOFT: the cross sections in order.
|
/// LOFT: the cross sections in order.
|
||||||
Loft { sections: Vec<Handle> },
|
Loft { sections: Vec<Handle> },
|
||||||
}
|
}
|
||||||
@@ -28,20 +40,31 @@ impl SurfaceSources {
|
|||||||
pub fn handles(&self) -> Vec<Handle> {
|
pub fn handles(&self) -> Vec<Handle> {
|
||||||
match self {
|
match self {
|
||||||
SurfaceSources::Extrude { profile, .. } => vec![*profile],
|
SurfaceSources::Extrude { profile, .. } => vec![*profile],
|
||||||
SurfaceSources::Revolve { profile, axis, .. } => std::iter::once(*profile).chain(*axis).collect(),
|
SurfaceSources::Revolve { profile, axis, .. } => {
|
||||||
|
std::iter::once(*profile).chain(*axis).collect()
|
||||||
|
}
|
||||||
SurfaceSources::Loft { sections } => sections.clone(),
|
SurfaceSources::Loft { sections } => sections.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keep the sources with the surface.
|
/// Keep the sources with the surface.
|
||||||
pub fn link_surface_sources(document: &mut codec::CadDocument, surface: Handle, sources: &SurfaceSources) {
|
pub fn link_surface_sources(
|
||||||
|
document: &mut codec::CadDocument,
|
||||||
|
surface: Handle,
|
||||||
|
sources: &SurfaceSources,
|
||||||
|
) {
|
||||||
if surface.is_null() {
|
if surface.is_null() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let point = |p: DVec3| XDataValue::Point3D(codec::types::Vector3::new(p.x, p.y, p.z));
|
let point = |p: DVec3| XDataValue::Point3D(codec::types::Vector3::new(p.x, p.y, p.z));
|
||||||
let values = match sources {
|
let values = match sources {
|
||||||
SurfaceSources::Extrude { profile, height, direction, taper } => {
|
SurfaceSources::Extrude {
|
||||||
|
profile,
|
||||||
|
height,
|
||||||
|
direction,
|
||||||
|
taper,
|
||||||
|
} => {
|
||||||
let mut values = vec![
|
let mut values = vec![
|
||||||
XDataValue::String("EXTRUDE".into()),
|
XDataValue::String("EXTRUDE".into()),
|
||||||
XDataValue::Handle(*profile),
|
XDataValue::Handle(*profile),
|
||||||
@@ -51,7 +74,14 @@ pub fn link_surface_sources(document: &mut codec::CadDocument, surface: Handle,
|
|||||||
values.extend(direction.map(point));
|
values.extend(direction.map(point));
|
||||||
values
|
values
|
||||||
}
|
}
|
||||||
SurfaceSources::Revolve { profile, axis, start, end, angle, start_angle } => {
|
SurfaceSources::Revolve {
|
||||||
|
profile,
|
||||||
|
axis,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
angle,
|
||||||
|
start_angle,
|
||||||
|
} => {
|
||||||
let mut values = vec![
|
let mut values = vec![
|
||||||
XDataValue::String("REVOLVE".into()),
|
XDataValue::String("REVOLVE".into()),
|
||||||
XDataValue::Handle(*profile),
|
XDataValue::Handle(*profile),
|
||||||
@@ -67,7 +97,12 @@ pub fn link_surface_sources(document: &mut codec::CadDocument, surface: Handle,
|
|||||||
.chain(sections.iter().map(|handle| XDataValue::Handle(*handle)))
|
.chain(sections.iter().map(|handle| XDataValue::Handle(*handle)))
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
crate::scene::view::dispatch::set_entity_xdata(document, surface, SURFACE_SOURCES_APP, Some(values));
|
crate::scene::view::dispatch::set_entity_xdata(
|
||||||
|
document,
|
||||||
|
surface,
|
||||||
|
SURFACE_SOURCES_APP,
|
||||||
|
Some(values),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The sources an associative surface keeps.
|
/// The sources an associative surface keeps.
|
||||||
|
|||||||
@@ -15,10 +15,17 @@ fn embedded_sweep_profile(
|
|||||||
) -> Option<(codec::entities::EmbeddedEntity, [f64; 16])> {
|
) -> Option<(codec::entities::EmbeddedEntity, [f64; 16])> {
|
||||||
match entity {
|
match entity {
|
||||||
// A region as the curve round it, as the reference records it.
|
// A region as the curve round it, as the reference records it.
|
||||||
EntityType::Region(region) => super::sweep_model::region_boundary_entity(entity).or_else(|| {
|
EntityType::Region(region) => {
|
||||||
Some((codec::entities::EmbeddedEntity::Region(region.clone()), glam::DMat4::IDENTITY.to_cols_array()))
|
super::sweep_model::region_boundary_entity(entity).or_else(|| {
|
||||||
}),
|
Some((
|
||||||
EntityType::Polyline3D(value) => Some((polyline_wire(value)?, glam::DMat4::IDENTITY.to_cols_array())),
|
codec::entities::EmbeddedEntity::Region(region.clone()),
|
||||||
|
glam::DMat4::IDENTITY.to_cols_array(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
EntityType::Polyline3D(value) => {
|
||||||
|
Some((polyline_wire(value)?, glam::DMat4::IDENTITY.to_cols_array()))
|
||||||
|
}
|
||||||
_ => embedded_revolve_profile(entity),
|
_ => embedded_revolve_profile(entity),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -725,20 +732,33 @@ pub fn assoc_edge_curve(entity: &EntityType) -> Option<AssocCurve> {
|
|||||||
// across the major one), major and minor radius, start and end
|
// across the major one), major and minor radius, start and end
|
||||||
// parameter, 0.
|
// parameter, 0.
|
||||||
EntityType::Ellipse(ellipse) => {
|
EntityType::Ellipse(ellipse) => {
|
||||||
let major = glam::DVec3::new(ellipse.major_axis.x, ellipse.major_axis.y, ellipse.major_axis.z);
|
let major = glam::DVec3::new(
|
||||||
let normal = glam::DVec3::new(ellipse.normal.x, ellipse.normal.y, ellipse.normal.z).try_normalize()?;
|
ellipse.major_axis.x,
|
||||||
|
ellipse.major_axis.y,
|
||||||
|
ellipse.major_axis.z,
|
||||||
|
);
|
||||||
|
let normal = glam::DVec3::new(ellipse.normal.x, ellipse.normal.y, ellipse.normal.z)
|
||||||
|
.try_normalize()?;
|
||||||
let radius = major.length();
|
let radius = major.length();
|
||||||
let axis = major.try_normalize()?;
|
let axis = major.try_normalize()?;
|
||||||
Some((17, K::Ellipse, vec![
|
Some((
|
||||||
point(glam::DVec3::new(ellipse.center.x, ellipse.center.y, ellipse.center.z)),
|
17,
|
||||||
point(axis),
|
K::Ellipse,
|
||||||
point(normal.cross(axis)),
|
vec![
|
||||||
V::Real(radius),
|
point(glam::DVec3::new(
|
||||||
V::Real(radius * ellipse.minor_axis_ratio),
|
ellipse.center.x,
|
||||||
V::Real(ellipse.start_parameter),
|
ellipse.center.y,
|
||||||
V::Real(ellipse.end_parameter),
|
ellipse.center.z,
|
||||||
V::Real(0.0),
|
)),
|
||||||
]))
|
point(axis),
|
||||||
|
point(normal.cross(axis)),
|
||||||
|
V::Real(radius),
|
||||||
|
V::Real(radius * ellipse.minor_axis_ratio),
|
||||||
|
V::Real(ellipse.start_parameter),
|
||||||
|
V::Real(ellipse.end_parameter),
|
||||||
|
V::Real(0.0),
|
||||||
|
],
|
||||||
|
))
|
||||||
}
|
}
|
||||||
// NURBS (42): rational flag, a second flag (0), degree, knot
|
// NURBS (42): rational flag, a second flag (0), degree, knot
|
||||||
// tolerance, then knots, weights (empty unless rational) and control
|
// tolerance, then knots, weights (empty unless rational) and control
|
||||||
@@ -749,20 +769,45 @@ pub fn assoc_edge_curve(entity: &EntityType) -> Option<AssocCurve> {
|
|||||||
// in the fit parameter (chord length) as the reference keeps them.
|
// in the fit parameter (chord length) as the reference keeps them.
|
||||||
let curve = crate::entities::spline::nurbs3(spline)?;
|
let curve = crate::entities::spline::nurbs3(spline)?;
|
||||||
let mut knots = curve.knots().to_vec();
|
let mut knots = curve.knots().to_vec();
|
||||||
if crate::entities::spline::uses_fit_method(spline) && spline.knot_parameterization == 0 {
|
if crate::entities::spline::uses_fit_method(spline) && spline.knot_parameterization == 0
|
||||||
let chord: f64 = spline.fit_points.windows(2)
|
{
|
||||||
.map(|pair| ((pair[1].x - pair[0].x).powi(2) + (pair[1].y - pair[0].y).powi(2) + (pair[1].z - pair[0].z).powi(2)).sqrt())
|
let chord: f64 = spline
|
||||||
|
.fit_points
|
||||||
|
.windows(2)
|
||||||
|
.map(|pair| {
|
||||||
|
((pair[1].x - pair[0].x).powi(2)
|
||||||
|
+ (pair[1].y - pair[0].y).powi(2)
|
||||||
|
+ (pair[1].z - pair[0].z).powi(2))
|
||||||
|
.sqrt()
|
||||||
|
})
|
||||||
.sum();
|
.sum();
|
||||||
let (low, high) = (knots[0], knots[knots.len() - 1]);
|
let (low, high) = (knots[0], knots[knots.len() - 1]);
|
||||||
if high - low > 0.0 && chord > 0.0 && (high - low - chord).abs() > 1e-9 * chord {
|
if high - low > 0.0 && chord > 0.0 && (high - low - chord).abs() > 1e-9 * chord {
|
||||||
knots.iter_mut().for_each(|knot| *knot = (*knot - low) / (high - low) * chord);
|
knots
|
||||||
|
.iter_mut()
|
||||||
|
.for_each(|knot| *knot = (*knot - low) / (high - low) * chord);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let points = curve.control_points();
|
let points = curve.control_points();
|
||||||
if points.len() < 2 || knots.is_empty() { return None; }
|
if points.len() < 2 || knots.is_empty() {
|
||||||
let tolerance = if spline.knot_tolerance > 0.0 { spline.knot_tolerance } else { 1e-9 };
|
return None;
|
||||||
let points = points.iter().map(|p| glam::DVec3::from_array(*p)).collect::<Vec<_>>();
|
}
|
||||||
Some(nurbs_curve(curve.degree(), tolerance, &knots, curve.weights(), &points))
|
let tolerance = if spline.knot_tolerance > 0.0 {
|
||||||
|
spline.knot_tolerance
|
||||||
|
} else {
|
||||||
|
1e-9
|
||||||
|
};
|
||||||
|
let points = points
|
||||||
|
.iter()
|
||||||
|
.map(|p| glam::DVec3::from_array(*p))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Some(nurbs_curve(
|
||||||
|
curve.degree(),
|
||||||
|
tolerance,
|
||||||
|
&knots,
|
||||||
|
curve.weights(),
|
||||||
|
&points,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -772,25 +817,48 @@ pub fn assoc_edge_curve(entity: &EntityType) -> Option<AssocCurve> {
|
|||||||
/// (0), degree, knot tolerance, then knots, weights (empty unless rational)
|
/// (0), degree, knot tolerance, then knots, weights (empty unless rational)
|
||||||
/// and control points, each as its length, the length reserved (at least
|
/// and control points, each as its length, the length reserved (at least
|
||||||
/// 8) and the growth step 8, followed by the items.
|
/// 8) and the growth step 8, followed by the items.
|
||||||
fn nurbs_curve(degree: usize, tolerance: f64, knots: &[f64], weights: &[f64], points: &[glam::DVec3]) -> AssocCurve {
|
fn nurbs_curve(
|
||||||
|
degree: usize,
|
||||||
|
tolerance: f64,
|
||||||
|
knots: &[f64],
|
||||||
|
weights: &[f64],
|
||||||
|
points: &[glam::DVec3],
|
||||||
|
) -> AssocCurve {
|
||||||
use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K};
|
use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K};
|
||||||
let rational = weights.iter().any(|weight| (weight - 1.0).abs() > 1e-12);
|
let rational = weights.iter().any(|weight| (weight - 1.0).abs() > 1e-12);
|
||||||
let header = |values: &mut Vec<V>, count: usize| {
|
let header = |values: &mut Vec<V>, count: usize| {
|
||||||
values.extend([V::Int(count as i32), V::Int(if count == 0 { 0 } else { count.max(8) } as i32), V::Int(8)]);
|
values.extend([
|
||||||
|
V::Int(count as i32),
|
||||||
|
V::Int(if count == 0 { 0 } else { count.max(8) } as i32),
|
||||||
|
V::Int(8),
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
let mut values = vec![V::Bool(rational), V::Bool(false), V::Int(degree as i32), V::Real(tolerance)];
|
let mut values = vec![
|
||||||
|
V::Bool(rational),
|
||||||
|
V::Bool(false),
|
||||||
|
V::Int(degree as i32),
|
||||||
|
V::Real(tolerance),
|
||||||
|
];
|
||||||
header(&mut values, knots.len());
|
header(&mut values, knots.len());
|
||||||
values.extend(knots.iter().map(|knot| V::Real(*knot)));
|
values.extend(knots.iter().map(|knot| V::Real(*knot)));
|
||||||
let weights = if rational { weights } else { &[] };
|
let weights = if rational { weights } else { &[] };
|
||||||
header(&mut values, weights.len());
|
header(&mut values, weights.len());
|
||||||
values.extend(weights.iter().map(|weight| V::Real(*weight)));
|
values.extend(weights.iter().map(|weight| V::Real(*weight)));
|
||||||
header(&mut values, points.len());
|
header(&mut values, points.len());
|
||||||
values.extend(points.iter().map(|p| V::Point(codec::types::Vector3::new(p.x, p.y, p.z))));
|
values.extend(
|
||||||
|
points
|
||||||
|
.iter()
|
||||||
|
.map(|p| V::Point(codec::types::Vector3::new(p.x, p.y, p.z))),
|
||||||
|
);
|
||||||
(42, K::Nurb3d, values)
|
(42, K::Nurb3d, values)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An edge parameter's curve: kind, subcurve kind and values.
|
/// An edge parameter's curve: kind, subcurve kind and values.
|
||||||
pub type AssocCurve = (i32, codec::objects::AssocSubcurveKind, Vec<codec::objects::AssocCurveValue>);
|
pub type AssocCurve = (
|
||||||
|
i32,
|
||||||
|
codec::objects::AssocSubcurveKind,
|
||||||
|
Vec<codec::objects::AssocCurveValue>,
|
||||||
|
);
|
||||||
|
|
||||||
/// A region's edges as an associative action reads them, loop by loop, each
|
/// A region's edges as an associative action reads them, loop by loop, each
|
||||||
/// loop from its second edge on as the reference lists them: lines (23),
|
/// loop from its second edge on as the reference lists them: lines (23),
|
||||||
@@ -799,23 +867,46 @@ pub type AssocCurve = (i32, codec::objects::AssocSubcurveKind, Vec<codec::object
|
|||||||
pub fn region_edge_curves(entity: &EntityType) -> Option<Vec<Vec<AssocCurve>>> {
|
pub fn region_edge_curves(entity: &EntityType) -> Option<Vec<Vec<AssocCurve>>> {
|
||||||
use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K};
|
use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K};
|
||||||
use kernel::geom2d::Curve;
|
use kernel::geom2d::Curve;
|
||||||
let EntityType::Region(_) = entity else { return None };
|
let EntityType::Region(_) = entity else {
|
||||||
let Some((plane, loops, true)) = super::presspull_model::profile_geometry(entity) else { return None };
|
return None;
|
||||||
|
};
|
||||||
|
let Some((plane, loops, true)) = super::presspull_model::profile_geometry(entity) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
let at = |uv: [f64; 2]| glam::DVec3::from_array(plane.point_at(uv));
|
let at = |uv: [f64; 2]| glam::DVec3::from_array(plane.point_at(uv));
|
||||||
let normal = glam::DVec3::from_array(plane.x_axis).cross(glam::DVec3::from_array(plane.y_axis)).try_normalize()?;
|
let normal = glam::DVec3::from_array(plane.x_axis)
|
||||||
|
.cross(glam::DVec3::from_array(plane.y_axis))
|
||||||
|
.try_normalize()?;
|
||||||
let axis = arbitrary_x(normal);
|
let axis = arbitrary_x(normal);
|
||||||
let across = normal.cross(axis);
|
let across = normal.cross(axis);
|
||||||
let point = |p: glam::DVec3| V::Point(codec::types::Vector3::new(p.x, p.y, p.z));
|
let point = |p: glam::DVec3| V::Point(codec::types::Vector3::new(p.x, p.y, p.z));
|
||||||
let arc = |centre: glam::DVec3, radius: f64, start: f64, end: f64| (11, K::Arc, vec![
|
let arc = |centre: glam::DVec3, radius: f64, start: f64, end: f64| {
|
||||||
point(centre), point(normal), point(axis), V::Real(radius), V::Real(start), V::Real(end), V::Real(0.0),
|
(
|
||||||
]);
|
11,
|
||||||
|
K::Arc,
|
||||||
|
vec![
|
||||||
|
point(centre),
|
||||||
|
point(normal),
|
||||||
|
point(axis),
|
||||||
|
V::Real(radius),
|
||||||
|
V::Real(start),
|
||||||
|
V::Real(end),
|
||||||
|
V::Real(0.0),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
};
|
||||||
let curve = |piece: &Curve| -> Option<AssocCurve> {
|
let curve = |piece: &Curve| -> Option<AssocCurve> {
|
||||||
match piece {
|
match piece {
|
||||||
Curve::Line(line) => {
|
Curve::Line(line) => {
|
||||||
let (a, b) = (at(line.start), at(line.end));
|
let (a, b) = (at(line.start), at(line.end));
|
||||||
Some((23, K::LineSegment3d, vec![point(a), point(b - a)]))
|
Some((23, K::LineSegment3d, vec![point(a), point(b - a)]))
|
||||||
}
|
}
|
||||||
Curve::Circle(circle) => Some(arc(at(circle.centre), circle.radius, 0.0, std::f64::consts::TAU)),
|
Curve::Circle(circle) => Some(arc(
|
||||||
|
at(circle.centre),
|
||||||
|
circle.radius,
|
||||||
|
0.0,
|
||||||
|
std::f64::consts::TAU,
|
||||||
|
)),
|
||||||
Curve::Arc(value) => {
|
Curve::Arc(value) => {
|
||||||
let centre = at(value.centre);
|
let centre = at(value.centre);
|
||||||
let offset = at([
|
let offset = at([
|
||||||
@@ -828,22 +919,44 @@ pub fn region_edge_curves(entity: &EntityType) -> Option<Vec<Vec<AssocCurve>>> {
|
|||||||
Curve::Ellipse(value) => {
|
Curve::Ellipse(value) => {
|
||||||
let ellipse = value.ellipse;
|
let ellipse = value.ellipse;
|
||||||
let centre = at(ellipse.centre);
|
let centre = at(ellipse.centre);
|
||||||
let tip = [ellipse.centre[0] + ellipse.major_axis[0], ellipse.centre[1] + ellipse.major_axis[1]];
|
let tip = [
|
||||||
|
ellipse.centre[0] + ellipse.major_axis[0],
|
||||||
|
ellipse.centre[1] + ellipse.major_axis[1],
|
||||||
|
];
|
||||||
let major = (at(tip) - centre).try_normalize()?;
|
let major = (at(tip) - centre).try_normalize()?;
|
||||||
let end = if value.end_parameter > value.start_parameter {
|
let end = if value.end_parameter > value.start_parameter {
|
||||||
value.end_parameter
|
value.end_parameter
|
||||||
} else {
|
} else {
|
||||||
value.end_parameter + std::f64::consts::TAU
|
value.end_parameter + std::f64::consts::TAU
|
||||||
};
|
};
|
||||||
Some((17, K::Ellipse, vec![
|
Some((
|
||||||
point(centre), point(major), point(normal.cross(major)),
|
17,
|
||||||
V::Real(ellipse.major_radius), V::Real(ellipse.minor_radius),
|
K::Ellipse,
|
||||||
V::Real(value.start_parameter), V::Real(end), V::Real(0.0),
|
vec![
|
||||||
]))
|
point(centre),
|
||||||
|
point(major),
|
||||||
|
point(normal.cross(major)),
|
||||||
|
V::Real(ellipse.major_radius),
|
||||||
|
V::Real(ellipse.minor_radius),
|
||||||
|
V::Real(value.start_parameter),
|
||||||
|
V::Real(end),
|
||||||
|
V::Real(0.0),
|
||||||
|
],
|
||||||
|
))
|
||||||
}
|
}
|
||||||
Curve::Nurbs(value) => {
|
Curve::Nurbs(value) => {
|
||||||
let points = value.control_points().iter().map(|p| at(*p)).collect::<Vec<_>>();
|
let points = value
|
||||||
Some(nurbs_curve(value.degree(), 1e-9, value.knots(), value.weights(), &points))
|
.control_points()
|
||||||
|
.iter()
|
||||||
|
.map(|p| at(*p))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Some(nurbs_curve(
|
||||||
|
value.degree(),
|
||||||
|
1e-9,
|
||||||
|
value.knots(),
|
||||||
|
value.weights(),
|
||||||
|
&points,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -868,7 +981,11 @@ pub fn assoc_source_supported(entity: &EntityType) -> bool {
|
|||||||
|
|
||||||
/// The arbitrary-axis X direction of an entity normal.
|
/// The arbitrary-axis X direction of an entity normal.
|
||||||
pub(crate) fn arbitrary_x(normal: glam::DVec3) -> glam::DVec3 {
|
pub(crate) fn arbitrary_x(normal: glam::DVec3) -> glam::DVec3 {
|
||||||
let world = if normal.x.abs() < 1.0 / 64.0 && normal.y.abs() < 1.0 / 64.0 { glam::DVec3::Y } else { glam::DVec3::Z };
|
let world = if normal.x.abs() < 1.0 / 64.0 && normal.y.abs() < 1.0 / 64.0 {
|
||||||
|
glam::DVec3::Y
|
||||||
|
} else {
|
||||||
|
glam::DVec3::Z
|
||||||
|
};
|
||||||
world.cross(normal).normalize()
|
world.cross(normal).normalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,12 @@ use kernel::geom2d::{Arc, Curve, EllipseArc, Line};
|
|||||||
use kernel::space::{PlanarCurve, Plane, Vec3};
|
use kernel::space::{PlanarCurve, Plane, Vec3};
|
||||||
|
|
||||||
pub use super::sweep_command_model::{
|
pub use super::sweep_command_model::{
|
||||||
is_sweep_path, is_sweep_profile, scaled_sweep_path_refusal, spatial_profile, spatial_sweep_record, surface_sweep_record, sweep_expressions, link_sweep_expressions, link_sweep_sources, sweep_sources, SWEEP_SOURCES_APP, resweep_from_sources, surface_associativity, set_surface_associativity, assoc_edge_curve, assoc_source_supported, region_edge_curves, AssocCurve, sweep_record, sweep_refusal_code, sweep_selection_options, swept_surface_entity, swept_with_options,
|
assoc_edge_curve, assoc_source_supported, is_sweep_path, is_sweep_profile,
|
||||||
|
link_sweep_expressions, link_sweep_sources, region_edge_curves, resweep_from_sources,
|
||||||
|
scaled_sweep_path_refusal, set_surface_associativity, spatial_profile, spatial_sweep_record,
|
||||||
|
surface_associativity, surface_sweep_record, sweep_expressions, sweep_record,
|
||||||
|
sweep_refusal_code, sweep_selection_options, sweep_sources, swept_surface_entity,
|
||||||
|
swept_with_options, AssocCurve, SWEEP_SOURCES_APP,
|
||||||
};
|
};
|
||||||
use crate::entities::curve::entity_curve;
|
use crate::entities::curve::entity_curve;
|
||||||
|
|
||||||
@@ -68,12 +73,16 @@ pub fn extrusion_profile_of(entity: &EntityType) -> Option<(Profile, bool)> {
|
|||||||
/// edges (its display wires hold one wire per edge), a lone closed edge split
|
/// edges (its display wires hold one wire per edge), a lone closed edge split
|
||||||
/// as a closed curve is.
|
/// as a closed curve is.
|
||||||
fn region_loop(entity: &EntityType) -> Option<Profile> {
|
fn region_loop(entity: &EntityType) -> Option<Profile> {
|
||||||
let EntityType::Region(region) = entity else { return None };
|
let EntityType::Region(region) = entity else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
if !region.acis_data.has_data() {
|
if !region.acis_data.has_data() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let (plane, loops, _) = super::presspull_model::profile_geometry(entity)?;
|
let (plane, loops, _) = super::presspull_model::profile_geometry(entity)?;
|
||||||
let [boundary] = loops.as_slice() else { return None };
|
let [boundary] = loops.as_slice() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
let single = boundary.len() == 1;
|
let single = boundary.len() == 1;
|
||||||
let pieces = boundary
|
let pieces = boundary
|
||||||
.iter()
|
.iter()
|
||||||
@@ -85,7 +94,8 @@ fn region_loop(entity: &EntityType) -> Option<Profile> {
|
|||||||
|
|
||||||
/// Whether a region has more than one boundary loop.
|
/// Whether a region has more than one boundary loop.
|
||||||
fn region_with_holes(entity: &EntityType) -> bool {
|
fn region_with_holes(entity: &EntityType) -> bool {
|
||||||
matches!(entity, EntityType::Region(_)) && region_profiles(entity).is_some_and(|(_, loops)| loops.len() > 1)
|
matches!(entity, EntityType::Region(_))
|
||||||
|
&& region_profiles(entity).is_some_and(|(_, loops)| loops.len() > 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn profile_pieces(curve: &Curve, closed: bool) -> Option<Vec<Curve>> {
|
fn profile_pieces(curve: &Curve, closed: bool) -> Option<Vec<Curve>> {
|
||||||
@@ -665,12 +675,16 @@ pub fn embedded_path(entity: &EntityType) -> Option<EmbeddedEntity> {
|
|||||||
/// polyline in its own coordinate system about the region's normal, read
|
/// polyline in its own coordinate system about the region's normal, read
|
||||||
/// from the region's modeler edges.
|
/// from the region's modeler edges.
|
||||||
pub fn region_boundary_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> {
|
pub fn region_boundary_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> {
|
||||||
let EntityType::Region(region) = entity else { return None };
|
let EntityType::Region(region) = entity else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
if !region.acis_data.has_data() {
|
if !region.acis_data.has_data() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let (plane, loops, _) = super::presspull_model::profile_geometry(entity)?;
|
let (plane, loops, _) = super::presspull_model::profile_geometry(entity)?;
|
||||||
let [boundary] = loops.as_slice() else { return None };
|
let [boundary] = loops.as_slice() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
let world = |v: [f64; 3]| Vector3::new(v[0], v[1], v[2]);
|
let world = |v: [f64; 3]| Vector3::new(v[0], v[1], v[2]);
|
||||||
let normal = world(plane.normal()?);
|
let normal = world(plane.normal()?);
|
||||||
let identity = glam::DMat4::IDENTITY.to_cols_array();
|
let identity = glam::DMat4::IDENTITY.to_cols_array();
|
||||||
@@ -715,18 +729,31 @@ pub fn region_boundary_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f
|
|||||||
}
|
}
|
||||||
// Walk the loop head to tail; an arc run against its direction turns
|
// Walk the loop head to tail; an arc run against its direction turns
|
||||||
// clockwise (negative bulge).
|
// clockwise (negative bulge).
|
||||||
let close = |a: [f64; 2], b: [f64; 2]| (a[0] - b[0]).hypot(a[1] - b[1]) <= 1e-9 * (1.0 + a[0].abs().max(a[1].abs()));
|
let close = |a: [f64; 2], b: [f64; 2]| {
|
||||||
|
(a[0] - b[0]).hypot(a[1] - b[1]) <= 1e-9 * (1.0 + a[0].abs().max(a[1].abs()))
|
||||||
|
};
|
||||||
let ends = |piece: &Curve| match piece {
|
let ends = |piece: &Curve| match piece {
|
||||||
Curve::Line(line) => Some((line.start, line.end, 0.0)),
|
Curve::Line(line) => Some((line.start, line.end, 0.0)),
|
||||||
Curve::Arc(arc) => {
|
Curve::Arc(arc) => {
|
||||||
let at = |angle: f64| [arc.centre[0] + arc.radius * angle.cos(), arc.centre[1] + arc.radius * angle.sin()];
|
let at = |angle: f64| {
|
||||||
Some((at(arc.start_angle), at(arc.end_angle), (arc.sweep() / 4.0).tan()))
|
[
|
||||||
|
arc.centre[0] + arc.radius * angle.cos(),
|
||||||
|
arc.centre[1] + arc.radius * angle.sin(),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
Some((
|
||||||
|
at(arc.start_angle),
|
||||||
|
at(arc.end_angle),
|
||||||
|
(arc.sweep() / 4.0).tan(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let (first_start, first_end, _) = ends(boundary.first()?)?;
|
let (first_start, first_end, _) = ends(boundary.first()?)?;
|
||||||
let second = boundary.get(1).and_then(ends);
|
let second = boundary.get(1).and_then(ends);
|
||||||
let mut at = if second.is_some_and(|(start, end, _)| close(first_start, start) || close(first_start, end)) {
|
let mut at = if second
|
||||||
|
.is_some_and(|(start, end, _)| close(first_start, start) || close(first_start, end))
|
||||||
|
{
|
||||||
first_end
|
first_end
|
||||||
} else {
|
} else {
|
||||||
first_start
|
first_start
|
||||||
@@ -740,7 +767,13 @@ pub fn region_boundary_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f
|
|||||||
polyline.elevation = glam::DVec3::from_array(plane.origin).dot(axis_normal);
|
polyline.elevation = glam::DVec3::from_array(plane.origin).dot(axis_normal);
|
||||||
for piece in boundary {
|
for piece in boundary {
|
||||||
let (start, end, bulge) = ends(piece)?;
|
let (start, end, bulge) = ends(piece)?;
|
||||||
let (from, to, bulge) = if close(start, at) { (start, end, bulge) } else if close(end, at) { (end, start, -bulge) } else { return None };
|
let (from, to, bulge) = if close(start, at) {
|
||||||
|
(start, end, bulge)
|
||||||
|
} else if close(end, at) {
|
||||||
|
(end, start, -bulge)
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
let point = glam::DVec3::from_array(plane.point_at(from));
|
let point = glam::DVec3::from_array(plane.point_at(from));
|
||||||
let mut vertex = LwVertex::new(Vector2::new(point.dot(axis_x), point.dot(axis_y)));
|
let mut vertex = LwVertex::new(Vector2::new(point.dot(axis_x), point.dot(axis_y)));
|
||||||
vertex.bulge = bulge;
|
vertex.bulge = bulge;
|
||||||
|
|||||||
+218
-60
@@ -1424,32 +1424,51 @@ impl Scene {
|
|||||||
/// their sources: made again from them with the values they were made
|
/// their sources: made again from them with the values they were made
|
||||||
/// with. A surface edited along with its sources keeps its own edit.
|
/// with. A surface edited along with its sources keeps its own edit.
|
||||||
/// Returns the surfaces rebuilt.
|
/// Returns the surfaces rebuilt.
|
||||||
pub(crate) fn refresh_associative_surfaces(&mut self, changes: &[(Handle, crate::scene::ChangeKind)]) -> Vec<(Handle, crate::scene::ChangeKind)> {
|
pub(crate) fn refresh_associative_surfaces(
|
||||||
use crate::scene::model::surface_sources::{axis_points, surface_sources, SurfaceSources, SURFACE_SOURCES_APP};
|
&mut self,
|
||||||
|
changes: &[(Handle, crate::scene::ChangeKind)],
|
||||||
|
) -> Vec<(Handle, crate::scene::ChangeKind)> {
|
||||||
|
use crate::scene::model::surface_sources::{
|
||||||
|
axis_points, surface_sources, SurfaceSources, SURFACE_SOURCES_APP,
|
||||||
|
};
|
||||||
use crate::scene::model::sweep_model;
|
use crate::scene::model::sweep_model;
|
||||||
use codec::objects::SolidHistoryOperation;
|
use codec::objects::SolidHistoryOperation;
|
||||||
let changed = changes.iter()
|
let changed = changes
|
||||||
|
.iter()
|
||||||
.filter(|(_, kind)| !matches!(kind, crate::scene::ChangeKind::Removed))
|
.filter(|(_, kind)| !matches!(kind, crate::scene::ChangeKind::Removed))
|
||||||
.map(|(handle, _)| *handle)
|
.map(|(handle, _)| *handle)
|
||||||
.collect::<rustc_hash::FxHashSet<_>>();
|
.collect::<rustc_hash::FxHashSet<_>>();
|
||||||
if changed.is_empty() || !self.document.app_ids.contains(SURFACE_SOURCES_APP) {
|
if changed.is_empty() || !self.document.app_ids.contains(SURFACE_SOURCES_APP) {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let linked = self.document.entities()
|
let linked = self
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
.filter(|entity| matches!(entity, EntityType::Surface(_)))
|
.filter(|entity| matches!(entity, EntityType::Surface(_)))
|
||||||
.filter_map(|entity| {
|
.filter_map(|entity| {
|
||||||
let handle = entity.common().handle;
|
let handle = entity.common().handle;
|
||||||
let sources = surface_sources(entity)?;
|
let sources = surface_sources(entity)?;
|
||||||
(!changed.contains(&handle) && sources.handles().iter().any(|source| changed.contains(source)))
|
(!changed.contains(&handle)
|
||||||
.then_some((handle, sources))
|
&& sources
|
||||||
|
.handles()
|
||||||
|
.iter()
|
||||||
|
.any(|source| changed.contains(source)))
|
||||||
|
.then_some((handle, sources))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut rebuilt = Vec::new();
|
let mut rebuilt = Vec::new();
|
||||||
for (handle, sources) in linked {
|
for (handle, sources) in linked {
|
||||||
let source = |handle: Handle| self.document.get_entity(handle).cloned();
|
let source = |handle: Handle| self.document.get_entity(handle).cloned();
|
||||||
// The surface made again: a history step, or a body with its data.
|
// The surface made again: a history step, or a body with its data.
|
||||||
let made: Option<Result<SolidHistoryOperation, (kernel::brep::Body, codec::entities::SurfaceData)>> = match &sources {
|
let made: Option<
|
||||||
SurfaceSources::Extrude { profile, height, direction, taper } => (|| {
|
Result<SolidHistoryOperation, (kernel::brep::Body, codec::entities::SurfaceData)>,
|
||||||
|
> = match &sources {
|
||||||
|
SurfaceSources::Extrude {
|
||||||
|
profile,
|
||||||
|
height,
|
||||||
|
direction,
|
||||||
|
taper,
|
||||||
|
} => (|| {
|
||||||
let entity = source(*profile)?;
|
let entity = source(*profile)?;
|
||||||
let (shape, _) = sweep_model::extrusion_profile_of(&entity)?;
|
let (shape, _) = sweep_model::extrusion_profile_of(&entity)?;
|
||||||
let direction = match direction {
|
let direction = match direction {
|
||||||
@@ -1457,37 +1476,86 @@ impl Scene {
|
|||||||
None => glam::DVec3::from_array(shape.plane.normal()?) * *height,
|
None => glam::DVec3::from_array(shape.plane.normal()?) * *height,
|
||||||
};
|
};
|
||||||
if self.document.solid_history_operations(handle).is_some() {
|
if self.document.solid_history_operations(handle).is_some() {
|
||||||
return sweep_model::extrusion_history(&entity, None, direction.to_array(), *taper, shape.plane.origin).map(Ok);
|
return sweep_model::extrusion_history(
|
||||||
|
&entity,
|
||||||
|
None,
|
||||||
|
direction.to_array(),
|
||||||
|
*taper,
|
||||||
|
shape.plane.origin,
|
||||||
|
)
|
||||||
|
.map(Ok);
|
||||||
}
|
}
|
||||||
let body = sweep_model::extruded_surface(&entity, direction.to_array(), *taper)?;
|
let body =
|
||||||
let EntityType::Surface(fresh) = crate::modules::insert::solid3d_cmds::empty_extruded_surface(direction, *taper) else { return None };
|
sweep_model::extruded_surface(&entity, direction.to_array(), *taper)?;
|
||||||
|
let EntityType::Surface(fresh) =
|
||||||
|
crate::modules::insert::solid3d_cmds::empty_extruded_surface(
|
||||||
|
direction, *taper,
|
||||||
|
)
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
Some(Err((body, fresh.surface_data)))
|
Some(Err((body, fresh.surface_data)))
|
||||||
})(),
|
})(),
|
||||||
SurfaceSources::Revolve { profile, axis, start, end, angle, start_angle } => (|| {
|
SurfaceSources::Revolve {
|
||||||
|
profile,
|
||||||
|
axis,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
angle,
|
||||||
|
start_angle,
|
||||||
|
} => (|| {
|
||||||
let entity = source(*profile)?;
|
let entity = source(*profile)?;
|
||||||
let (start, end) = axis
|
let (start, end) = axis
|
||||||
.and_then(|axis| self.document.get_entity(axis).and_then(axis_points))
|
.and_then(|axis| self.document.get_entity(axis).and_then(axis_points))
|
||||||
.unwrap_or((*start, *end));
|
.unwrap_or((*start, *end));
|
||||||
let body = sweep_model::revolved_surface(&entity, start.to_array(), end.to_array(), *angle, *start_angle)?;
|
let body = sweep_model::revolved_surface(
|
||||||
let EntityType::Surface(fresh) = crate::modules::insert::solid3d_cmds::empty_revolved_surface(&entity, start, end, *angle, *start_angle) else { return None };
|
&entity,
|
||||||
|
start.to_array(),
|
||||||
|
end.to_array(),
|
||||||
|
*angle,
|
||||||
|
*start_angle,
|
||||||
|
)?;
|
||||||
|
let EntityType::Surface(fresh) =
|
||||||
|
crate::modules::insert::solid3d_cmds::empty_revolved_surface(
|
||||||
|
&entity,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
*angle,
|
||||||
|
*start_angle,
|
||||||
|
)
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
Some(Err((body, fresh.surface_data)))
|
Some(Err((body, fresh.surface_data)))
|
||||||
})(),
|
})(),
|
||||||
SurfaceSources::Loft { sections } => (|| {
|
SurfaceSources::Loft { sections } => (|| {
|
||||||
let operations = self.document.solid_history_operations(handle)?;
|
let operations = self.document.solid_history_operations(handle)?;
|
||||||
let mut record = operations.into_iter().find_map(|operation| match operation {
|
let mut record =
|
||||||
SolidHistoryOperation::Loft(record) => Some(record),
|
operations
|
||||||
_ => None,
|
.into_iter()
|
||||||
})?;
|
.find_map(|operation| match operation {
|
||||||
|
SolidHistoryOperation::Loft(record) => Some(record),
|
||||||
|
_ => None,
|
||||||
|
})?;
|
||||||
if record.cross_sections.len() != sections.len() {
|
if record.cross_sections.len() != sections.len() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
record.cross_sections = sections.iter()
|
record.cross_sections = sections
|
||||||
.map(|section| source(*section).as_ref().and_then(crate::scene::model::loft_command_model::embedded_section))
|
.iter()
|
||||||
|
.map(|section| {
|
||||||
|
source(*section)
|
||||||
|
.as_ref()
|
||||||
|
.and_then(crate::scene::model::loft_command_model::embedded_section)
|
||||||
|
})
|
||||||
.collect::<Option<Vec<_>>>()?;
|
.collect::<Option<Vec<_>>>()?;
|
||||||
Some(Ok(SolidHistoryOperation::Loft(record)))
|
Some(Ok(SolidHistoryOperation::Loft(record)))
|
||||||
})(),
|
})(),
|
||||||
};
|
};
|
||||||
let before = self.document.get_entity(handle).cloned().map(std::sync::Arc::new);
|
let before = self
|
||||||
|
.document
|
||||||
|
.get_entity(handle)
|
||||||
|
.cloned()
|
||||||
|
.map(std::sync::Arc::new);
|
||||||
match made {
|
match made {
|
||||||
Some(Ok(operation)) => {
|
Some(Ok(operation)) => {
|
||||||
self.record_undo_before(handle, before);
|
self.record_undo_before(handle, before);
|
||||||
@@ -1496,11 +1564,18 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Err((body, data))) => {
|
Some(Err((body, data))) => {
|
||||||
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else { continue };
|
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body)
|
||||||
let Some(display) = self.prepare_solid_model_display(handle, &body) else { continue };
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(display) = self.prepare_solid_model_display(handle, &body) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
self.record_undo_before(handle, before);
|
self.record_undo_before(handle, before);
|
||||||
if let Some(EntityType::Surface(surface)) = self.document.get_entity_mut(handle) {
|
if let Some(EntityType::Surface(surface)) = self.document.get_entity_mut(handle)
|
||||||
surface.acis_data = codec::entities::AcisData::from_sat(&document.to_sat_string());
|
{
|
||||||
|
surface.acis_data =
|
||||||
|
codec::entities::AcisData::from_sat(&document.to_sat_string());
|
||||||
surface.surface_data = data;
|
surface.surface_data = data;
|
||||||
}
|
}
|
||||||
self.register_prepared_solid_model(handle, body, display);
|
self.register_prepared_solid_model(handle, body, display);
|
||||||
@@ -1525,42 +1600,90 @@ impl Scene {
|
|||||||
let mut sources = Vec::new();
|
let mut sources = Vec::new();
|
||||||
let mut others = Vec::new();
|
let mut others = Vec::new();
|
||||||
for item in self.document.objects.values() {
|
for item in self.document.objects.values() {
|
||||||
let ObjectType::Associative(action) = item else { continue };
|
let ObjectType::Associative(action) = item else {
|
||||||
let AssociativeData::Action(action) = &action.data else { continue };
|
continue;
|
||||||
let Some(AssociativeData::SurfaceActionBody(body)) = object(action.action_body) else { continue };
|
};
|
||||||
let Some(AssociativeData::Dependency(written)) = object(body.surface_body.dependency) else { continue };
|
let AssociativeData::Action(action) = &action.data else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(AssociativeData::SurfaceActionBody(body)) = object(action.action_body) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(AssociativeData::Dependency(written)) = object(body.surface_body.dependency)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
// The entities a named path parameter reads (an edge of a region
|
// The entities a named path parameter reads (an edge of a region
|
||||||
// reads through a geometry dependency).
|
// reads through a geometry dependency).
|
||||||
let read = |name: &str| -> Vec<Handle> {
|
let read = |name: &str| -> Vec<Handle> {
|
||||||
action.owned_parameters.iter().filter_map(|parameter| {
|
action
|
||||||
let Some(AssociativeData::PathActionParam(path)) = object(*parameter) else { return None };
|
.owned_parameters
|
||||||
if path.compound.action_param.name != name { return None; }
|
.iter()
|
||||||
let Some(AssociativeData::EdgeActionParam(edge)) = object(*path.compound.parameters.first()?) else { return None };
|
.filter_map(|parameter| {
|
||||||
match object(edge.single_dependency.dependency) {
|
let Some(AssociativeData::PathActionParam(path)) = object(*parameter)
|
||||||
Some(AssociativeData::Dependency(dependency)) => Some(dependency.dependent_on),
|
else {
|
||||||
Some(AssociativeData::GeomDependency(dependency)) => Some(dependency.dependency.dependent_on),
|
return None;
|
||||||
|
};
|
||||||
|
if path.compound.action_param.name != name {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let Some(AssociativeData::EdgeActionParam(edge)) =
|
||||||
|
object(*path.compound.parameters.first()?)
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
match object(edge.single_dependency.dependency) {
|
||||||
|
Some(AssociativeData::Dependency(dependency)) => {
|
||||||
|
Some(dependency.dependent_on)
|
||||||
|
}
|
||||||
|
Some(AssociativeData::GeomDependency(dependency)) => {
|
||||||
|
Some(dependency.dependency.dependent_on)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let real = |name: &str| {
|
||||||
|
action
|
||||||
|
.values
|
||||||
|
.iter()
|
||||||
|
.find(|value| value.name == name)
|
||||||
|
.and_then(|value| value.variables.first())
|
||||||
|
.and_then(|variable| match variable.value.value {
|
||||||
|
codec::objects::AssocEvalValue::Real(value) => Some(value),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
})
|
||||||
}).collect()
|
|
||||||
};
|
};
|
||||||
let real = |name: &str| action.values.iter().find(|value| value.name == name)
|
|
||||||
.and_then(|value| value.variables.first())
|
|
||||||
.and_then(|variable| match variable.value.value {
|
|
||||||
codec::objects::AssocEvalValue::Real(value) => Some(value),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
match body.kind {
|
match body.kind {
|
||||||
AssocSurfaceActionKind::Extruded => {
|
AssocSurfaceActionKind::Extruded => {
|
||||||
if let (Some(profile), Some(height), Some(taper)) =
|
if let (Some(profile), Some(height), Some(taper)) = (
|
||||||
(read("ExtrudeProfile").first().copied(), real("ExtrusionHeight"), real("ExtrusionTaperAngle"))
|
read("ExtrudeProfile").first().copied(),
|
||||||
{
|
real("ExtrusionHeight"),
|
||||||
others.push((written.dependent_on, (body.kind, vec![profile], None, height, taper)));
|
real("ExtrusionTaperAngle"),
|
||||||
|
) {
|
||||||
|
others.push((
|
||||||
|
written.dependent_on,
|
||||||
|
(body.kind, vec![profile], None, height, taper),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
AssocSurfaceActionKind::Revolved => {
|
AssocSurfaceActionKind::Revolved => {
|
||||||
if let (Some(profile), Some(angle)) = (read("RevolveProfile").first().copied(), real("RevolveAngle")) {
|
if let (Some(profile), Some(angle)) = (
|
||||||
others.push((written.dependent_on, (body.kind, vec![profile], read("RevolvePath").first().copied(), angle, 0.0)));
|
read("RevolveProfile").first().copied(),
|
||||||
|
real("RevolveAngle"),
|
||||||
|
) {
|
||||||
|
others.push((
|
||||||
|
written.dependent_on,
|
||||||
|
(
|
||||||
|
body.kind,
|
||||||
|
vec![profile],
|
||||||
|
read("RevolvePath").first().copied(),
|
||||||
|
angle,
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1603,7 +1726,10 @@ impl Scene {
|
|||||||
links.push((written.dependent_on, expressions));
|
links.push((written.dependent_on, expressions));
|
||||||
}
|
}
|
||||||
// The profile and path the action reads.
|
// The profile and path the action reads.
|
||||||
if let (Some(profile), Some(path)) = (read("SweepProfile").first().copied(), read("SweepPath").first().copied()) {
|
if let (Some(profile), Some(path)) = (
|
||||||
|
read("SweepProfile").first().copied(),
|
||||||
|
read("SweepPath").first().copied(),
|
||||||
|
) {
|
||||||
sources.push((written.dependent_on, profile, path));
|
sources.push((written.dependent_on, profile, path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1646,27 +1772,59 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
// Associative extruded, revolved and lofted surfaces follow their
|
// Associative extruded, revolved and lofted surfaces follow their
|
||||||
// sources here too.
|
// sources here too.
|
||||||
use crate::scene::model::surface_sources::{link_surface_sources, surface_sources, SurfaceSources};
|
use crate::scene::model::surface_sources::{
|
||||||
|
link_surface_sources, surface_sources, SurfaceSources,
|
||||||
|
};
|
||||||
for (surface, (kind, handles, axis, amount, taper)) in others {
|
for (surface, (kind, handles, axis, amount, taper)) in others {
|
||||||
let Some(entity @ EntityType::Surface(value)) = self.document.get_entity(surface) else { continue };
|
let Some(entity @ EntityType::Surface(value)) = self.document.get_entity(surface)
|
||||||
if surface_sources(entity).is_some() { continue; }
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if surface_sources(entity).is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let linked = match (kind, &value.surface_data) {
|
let linked = match (kind, &value.surface_data) {
|
||||||
(AssocSurfaceActionKind::Extruded, codec::entities::SurfaceData::Extruded { sweep_vector, .. }) => {
|
(
|
||||||
|
AssocSurfaceActionKind::Extruded,
|
||||||
|
codec::entities::SurfaceData::Extruded { sweep_vector, .. },
|
||||||
|
) => {
|
||||||
let profile = handles[0];
|
let profile = handles[0];
|
||||||
let along = glam::DVec3::new(sweep_vector.x, sweep_vector.y, sweep_vector.z);
|
let along = glam::DVec3::new(sweep_vector.x, sweep_vector.y, sweep_vector.z);
|
||||||
// A direction other than the profile's normal is kept.
|
// A direction other than the profile's normal is kept.
|
||||||
let normal = self.document.get_entity(profile)
|
let normal = self
|
||||||
.and_then(|entity| crate::scene::model::sweep_model::extrusion_profile_of(entity))
|
.document
|
||||||
|
.get_entity(profile)
|
||||||
|
.and_then(|entity| {
|
||||||
|
crate::scene::model::sweep_model::extrusion_profile_of(entity)
|
||||||
|
})
|
||||||
.and_then(|(shape, _)| shape.plane.normal())
|
.and_then(|(shape, _)| shape.plane.normal())
|
||||||
.map(glam::DVec3::from_array);
|
.map(glam::DVec3::from_array);
|
||||||
let direction = match normal {
|
let direction = match normal {
|
||||||
Some(normal) if (normal * amount - along).length() <= 1e-9 * amount.abs().max(1.0) => None,
|
Some(normal)
|
||||||
|
if (normal * amount - along).length()
|
||||||
|
<= 1e-9 * amount.abs().max(1.0) =>
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
_ if along.length() > 1e-12 => Some(along),
|
_ if along.length() > 1e-12 => Some(along),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
SurfaceSources::Extrude { profile, height: amount, direction, taper }
|
SurfaceSources::Extrude {
|
||||||
|
profile,
|
||||||
|
height: amount,
|
||||||
|
direction,
|
||||||
|
taper,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(AssocSurfaceActionKind::Revolved, codec::entities::SurfaceData::Revolved { axis_point, axis_vector, start_angle, .. }) => {
|
(
|
||||||
|
AssocSurfaceActionKind::Revolved,
|
||||||
|
codec::entities::SurfaceData::Revolved {
|
||||||
|
axis_point,
|
||||||
|
axis_vector,
|
||||||
|
start_angle,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => {
|
||||||
let start = glam::DVec3::new(axis_point.x, axis_point.y, axis_point.z);
|
let start = glam::DVec3::new(axis_point.x, axis_point.y, axis_point.z);
|
||||||
SurfaceSources::Revolve {
|
SurfaceSources::Revolve {
|
||||||
profile: handles[0],
|
profile: handles[0],
|
||||||
|
|||||||
+4
-1
@@ -604,7 +604,10 @@ impl Scene {
|
|||||||
_ => model.clone(),
|
_ => model.clone(),
|
||||||
};
|
};
|
||||||
let style = self.render_style(entity);
|
let style = self.render_style(entity);
|
||||||
if matches!(hatch.pattern, model::hatch_model::HatchPattern::Gradient { .. }) {
|
if matches!(
|
||||||
|
hatch.pattern,
|
||||||
|
model::hatch_model::HatchPattern::Gradient { .. }
|
||||||
|
) {
|
||||||
hatch.set_alpha(style.0[3]);
|
hatch.set_alpha(style.0[3]);
|
||||||
} else {
|
} else {
|
||||||
hatch.color = style.0;
|
hatch.color = style.0;
|
||||||
|
|||||||
@@ -3800,10 +3800,13 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|hatch| hatch.boundary.len() >= 3)
|
.filter(|hatch| hatch.boundary.len() >= 3)
|
||||||
.count();
|
.count();
|
||||||
let (translucent, opaque): (Vec<HatchModel>, Vec<HatchModel>) =
|
let (translucent, opaque): (Vec<HatchModel>, Vec<HatchModel>) = hatches
|
||||||
hatches.iter().cloned().partition(|hatch| hatch.color[3] < 0.999);
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.partition(|hatch| hatch.color[3] < 0.999);
|
||||||
self.hatch_gpu.upload(device, queue, &opaque);
|
self.hatch_gpu.upload(device, queue, &opaque);
|
||||||
self.hatch_translucent_gpu.upload(device, queue, &translucent);
|
self.hatch_translucent_gpu
|
||||||
|
.upload(device, queue, &translucent);
|
||||||
if let Some(started) = perf_started {
|
if let Some(started) = perf_started {
|
||||||
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||||
if elapsed_ms >= 1.0 {
|
if elapsed_ms >= 1.0 {
|
||||||
@@ -4853,8 +4856,11 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
|||||||
// Transparent hatches blend over the lines already drawn; the
|
// Transparent hatches blend over the lines already drawn; the
|
||||||
// lines in front of them still win the depth test.
|
// lines in front of them still win the depth test.
|
||||||
if !self.skip_hatch_frame {
|
if !self.skip_hatch_frame {
|
||||||
self.hatch_translucent_gpu
|
self.hatch_translucent_gpu.draw(
|
||||||
.draw(&mut pass, &self.uniform_bind_group, stencil_ref);
|
&mut pass,
|
||||||
|
&self.uniform_bind_group,
|
||||||
|
stencil_ref,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5656,10 +5662,7 @@ impl MultiPipeline {
|
|||||||
total.wire_arena += slot.wire_arena;
|
total.wire_arena += slot.wire_arena;
|
||||||
total.block_geometry +=
|
total.block_geometry +=
|
||||||
wire_gpu::block_arena_bytes(&inner.gpu_block_wires, &mut counted)
|
wire_gpu::block_arena_bytes(&inner.gpu_block_wires, &mut counted)
|
||||||
+ wire_gpu::block_arena_bytes(
|
+ wire_gpu::block_arena_bytes(&inner.gpu_selected_block_wires, &mut counted);
|
||||||
&inner.gpu_selected_block_wires,
|
|
||||||
&mut counted,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
for entry in self.wire_buffer_cache.values() {
|
for entry in self.wire_buffer_cache.values() {
|
||||||
total.block_geometry += wire_gpu::block_arena_bytes(&entry.1, &mut counted);
|
total.block_geometry += wire_gpu::block_arena_bytes(&entry.1, &mut counted);
|
||||||
|
|||||||
@@ -508,9 +508,7 @@ pub fn block_const_bind_group_layout(
|
|||||||
ty: wgpu::BindingType::Buffer {
|
ty: wgpu::BindingType::Buffer {
|
||||||
ty: wgpu::BufferBindingType::Uniform,
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
has_dynamic_offset: true,
|
has_dynamic_offset: true,
|
||||||
min_binding_size: std::num::NonZeroU64::new(
|
min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::<WireConst>() as u64),
|
||||||
std::mem::size_of::<WireConst>() as u64,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
}];
|
}];
|
||||||
@@ -1155,10 +1153,7 @@ pub struct BlockWireTarget<'a> {
|
|||||||
/// Device bytes the arenas behind these batches hold, counting each arena once
|
/// Device bytes the arenas behind these batches hold, counting each arena once
|
||||||
/// however many batches draw out of it. `seen` carries the arenas already
|
/// however many batches draw out of it. `seen` carries the arenas already
|
||||||
/// counted, so slots that share a cached build are not counted twice.
|
/// counted, so slots that share a cached build are not counted twice.
|
||||||
pub fn block_arena_bytes(
|
pub fn block_arena_bytes(batches: &[BlockWireGpu], seen: &mut rustc_hash::FxHashSet<usize>) -> u64 {
|
||||||
batches: &[BlockWireGpu],
|
|
||||||
seen: &mut rustc_hash::FxHashSet<usize>,
|
|
||||||
) -> u64 {
|
|
||||||
batches
|
batches
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|batch| seen.insert(std::sync::Arc::as_ptr(&batch.arena) as usize))
|
.filter(|batch| seen.insert(std::sync::Arc::as_ptr(&batch.arena) as usize))
|
||||||
@@ -1209,8 +1204,7 @@ impl BlockWireGpu {
|
|||||||
let mut groups: Vec<(bool, Vec<BlockRef<'_>>)> = Vec::new();
|
let mut groups: Vec<(bool, Vec<BlockRef<'_>>)> = Vec::new();
|
||||||
for &block in wires {
|
for &block in wires {
|
||||||
let wire = block.wire;
|
let wire = block.wire;
|
||||||
if color_override.is_none()
|
if color_override.is_none() && !(wire.display_visible && block.named().display_visible)
|
||||||
&& !(wire.display_visible && block.named().display_visible)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1279,8 +1273,7 @@ impl BlockWireGpu {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
emitted_segments += records.len();
|
emitted_segments += records.len();
|
||||||
let built =
|
let built = std::sync::Arc::new(BlockGeometryRecords { constant, records });
|
||||||
std::sync::Arc::new(BlockGeometryRecords { constant, records });
|
|
||||||
// Records are device-independent, so a build the device
|
// Records are device-independent, so a build the device
|
||||||
// rejected poisons nothing: the next one packs these same
|
// rejected poisons nothing: the next one packs these same
|
||||||
// records into fresh buffers instead of caching a buffer
|
// records into fresh buffers instead of caching a buffer
|
||||||
@@ -1306,7 +1299,11 @@ impl BlockWireGpu {
|
|||||||
crate::perf_record!(
|
crate::perf_record!(
|
||||||
"[perf] block-wire-geometry mode={} definitions={} arenas={} batches={} \
|
"[perf] block-wire-geometry mode={} definitions={} arenas={} batches={} \
|
||||||
segments={} bytes={} bytes_if_per_definition={} emitted={}",
|
segments={} bytes={} bytes_if_per_definition={} emitted={}",
|
||||||
if mode.uses_storage() { "storage" } else { "packed" },
|
if mode.uses_storage() {
|
||||||
|
"storage"
|
||||||
|
} else {
|
||||||
|
"packed"
|
||||||
|
},
|
||||||
live_keys.len(),
|
live_keys.len(),
|
||||||
arena.arenas,
|
arena.arenas,
|
||||||
out.len(),
|
out.len(),
|
||||||
|
|||||||
@@ -663,7 +663,9 @@ pub(crate) fn dimension_picture_placement(
|
|||||||
/// placed picture has its placement folded into the subs and the dimension's
|
/// placed picture has its placement folded into the subs and the dimension's
|
||||||
/// own placement cleared (`clear_dim_picture_placement`); a placement left on
|
/// own placement cleared (`clear_dim_picture_placement`); a placement left on
|
||||||
/// the dimension would be applied again on top of the moved subs.
|
/// the dimension would be applied again on top of the moved subs.
|
||||||
pub(crate) fn dim_picture_placement(entity: &EntityType) -> Option<crate::command::EntityTransform> {
|
pub(crate) fn dim_picture_placement(
|
||||||
|
entity: &EntityType,
|
||||||
|
) -> Option<crate::command::EntityTransform> {
|
||||||
let EntityType::Dimension(d) = entity else {
|
let EntityType::Dimension(d) = entity else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -71,7 +71,11 @@ impl Scene {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
if let Some(handle) = self.selected_handles_in_order().into_iter().find(is_section) {
|
if let Some(handle) = self
|
||||||
|
.selected_handles_in_order()
|
||||||
|
.into_iter()
|
||||||
|
.find(is_section)
|
||||||
|
{
|
||||||
return Some(handle);
|
return Some(handle);
|
||||||
}
|
}
|
||||||
let block = self.current_layout_block_handle();
|
let block = self.current_layout_block_handle();
|
||||||
@@ -102,25 +106,15 @@ impl Scene {
|
|||||||
let codec::entities::ExtendedEntityData::SectionObject(data) = &extended.data else {
|
let codec::entities::ExtendedEntityData::SectionObject(data) = &extended.data else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let slice_depth = crate::entities::extended::section_is_slice(extended).then(|| {
|
let slice_depth = crate::entities::extended::section_is_slice(extended)
|
||||||
crate::entities::extended::section_slice_depth(extended).unwrap_or(0.0)
|
.then(|| crate::entities::extended::section_slice_depth(extended).unwrap_or(0.0));
|
||||||
});
|
|
||||||
let planes = Self::section_planes(data, slice_depth, None)?;
|
let planes = Self::section_planes(data, slice_depth, None)?;
|
||||||
|
|
||||||
// Every drawn point of every object in the space, from its wires and
|
// Every drawn point of every object in the space, from its wires and
|
||||||
// its shaded mesh.
|
// its shaded mesh.
|
||||||
let block = self.current_layout_block_handle();
|
let block = self.current_layout_block_handle();
|
||||||
let mut points: FxHashMap<Handle, Vec<[f64; 3]>> = FxHashMap::default();
|
let mut points: FxHashMap<Handle, Vec<[f64; 3]>> = FxHashMap::default();
|
||||||
let wires = self.wires_for_block_culled(
|
let wires = self.wires_for_block_culled(block, None, None, None, None, None, true, None);
|
||||||
block,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
true,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
for wire in &wires {
|
for wire in &wires {
|
||||||
let Some(handle) = Self::handle_from_wire_name(&wire.name) else {
|
let Some(handle) = Self::handle_from_wire_name(&wire.name) else {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ impl Scene {
|
|||||||
.iter()
|
.iter()
|
||||||
.flat_map(|&index| [index, (index + 1) % count])
|
.flat_map(|&index| [index, (index + 1) % count])
|
||||||
.collect();
|
.collect();
|
||||||
let Some(plane) = crate::entities::curve::lwpolyline_curve(polyline)
|
let Some(plane) =
|
||||||
.map(|curve| curve.plane)
|
crate::entities::curve::lwpolyline_curve(polyline).map(|curve| curve.plane)
|
||||||
else {
|
else {
|
||||||
whole.push(handle);
|
whole.push(handle);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ fn fonts() -> &'static SysFonts {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Sorted, de-duplicated family names of every face currently in `db`.
|
/// Sorted, de-duplicated family names of every face currently in `db`.
|
||||||
fn family_names(db: &fontdb::Database) -> Vec<String> {
|
fn family_names(db: &fontdb::Database) -> Vec<String> {
|
||||||
let mut families: Vec<String> = db
|
let mut families: Vec<String> = db
|
||||||
@@ -446,14 +445,19 @@ mod tests {
|
|||||||
/// and whether it comes as a bare name or as a path.
|
/// and whether it comes as a bare name or as a path.
|
||||||
#[test]
|
#[test]
|
||||||
fn face_files_resolve_to_their_family() {
|
fn face_files_resolve_to_their_family() {
|
||||||
let found = fonts().db.lock().unwrap().faces().find_map(|face| match &face.source {
|
let found = fonts()
|
||||||
fontdb::Source::File(path) => {
|
.db
|
||||||
let file = path.file_name()?.to_string_lossy().into_owned();
|
.lock()
|
||||||
let family = face.families.first().map(|(name, _)| name.clone())?;
|
.unwrap()
|
||||||
Some((file, family))
|
.faces()
|
||||||
}
|
.find_map(|face| match &face.source {
|
||||||
_ => None,
|
fontdb::Source::File(path) => {
|
||||||
});
|
let file = path.file_name()?.to_string_lossy().into_owned();
|
||||||
|
let family = face.families.first().map(|(name, _)| name.clone())?;
|
||||||
|
Some((file, family))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
let Some((file, family)) = found else {
|
let Some((file, family)) = found else {
|
||||||
eprintln!("no installed face files on this host; skipping");
|
eprintln!("no installed face files on this host; skipping");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2044,7 +2044,8 @@ text_atlas={:.1} wire_arena={:.1} block_geometry={:.1}",
|
|||||||
impl Scene {
|
impl Scene {
|
||||||
/// The model-space viewport record of the active tile.
|
/// The model-space viewport record of the active tile.
|
||||||
pub(crate) fn active_tile_vport_handle(&self) -> Option<Handle> {
|
pub(crate) fn active_tile_vport_handle(&self) -> Option<Handle> {
|
||||||
self.model_tile_vport(self.active_model_tile.get()).map(|value| value.handle)
|
self.model_tile_vport(self.active_model_tile.get())
|
||||||
|
.map(|value| value.handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn model_tile_vport(&self, index: usize) -> Option<&codec::tables::VPort> {
|
fn model_tile_vport(&self, index: usize) -> Option<&codec::tables::VPort> {
|
||||||
@@ -2562,7 +2563,12 @@ impl Scene {
|
|||||||
if let Some(EntityType::Viewport(value)) = self.document.get_entity(handle) {
|
if let Some(EntityType::Viewport(value)) = self.document.get_entity(handle) {
|
||||||
// The sheet keeps its page and desk: a paper-space view's
|
// The sheet keeps its page and desk: a paper-space view's
|
||||||
// background is kept with the layout but not drawn.
|
// background is kept with the layout but not drawn.
|
||||||
return build(value.visual_style_handle, value.brightness, value.contrast, Handle::NULL);
|
return build(
|
||||||
|
value.visual_style_handle,
|
||||||
|
value.brightness,
|
||||||
|
value.contrast,
|
||||||
|
Handle::NULL,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if viewport.handle.is_valid() {
|
} else if viewport.handle.is_valid() {
|
||||||
if let Some(EntityType::Viewport(value)) = self.document.get_entity(viewport.handle) {
|
if let Some(EntityType::Viewport(value)) = self.document.get_entity(viewport.handle) {
|
||||||
@@ -2571,7 +2577,11 @@ impl Scene {
|
|||||||
value.visual_style_handle,
|
value.visual_style_handle,
|
||||||
value.brightness,
|
value.brightness,
|
||||||
value.contrast,
|
value.contrast,
|
||||||
if flat { Handle::NULL } else { value.background_handle },
|
if flat {
|
||||||
|
Handle::NULL
|
||||||
|
} else {
|
||||||
|
value.background_handle
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if self.current_layout == "Model" {
|
} else if self.current_layout == "Model" {
|
||||||
@@ -2585,7 +2595,11 @@ impl Scene {
|
|||||||
value.visual_style_handle,
|
value.visual_style_handle,
|
||||||
value.brightness,
|
value.brightness,
|
||||||
value.contrast,
|
value.contrast,
|
||||||
if flat { Handle::NULL } else { value.background_handle },
|
if flat {
|
||||||
|
Handle::NULL
|
||||||
|
} else {
|
||||||
|
value.background_handle
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+155
-44
@@ -141,7 +141,8 @@ pub fn parse_snap_override(input: &str) -> Option<SnapOverrideKind> {
|
|||||||
return Some(SnapOverrideKind::Mode(SnapType::Center));
|
return Some(SnapOverrideKind::Mode(SnapType::Center));
|
||||||
}
|
}
|
||||||
if (s.starts_with("gce") && "gcen".starts_with(&s))
|
if (s.starts_with("gce") && "gcen".starts_with(&s))
|
||||||
|| (s.starts_with("geo") && ("geocenter".starts_with(&s) || "geometriccenter".starts_with(&s)))
|
|| (s.starts_with("geo")
|
||||||
|
&& ("geocenter".starts_with(&s) || "geometriccenter".starts_with(&s)))
|
||||||
{
|
{
|
||||||
return Some(SnapOverrideKind::Mode(SnapType::GeometricCenter));
|
return Some(SnapOverrideKind::Mode(SnapType::GeometricCenter));
|
||||||
}
|
}
|
||||||
@@ -2209,7 +2210,8 @@ impl Snapper {
|
|||||||
let mut exact = false;
|
let mut exact = false;
|
||||||
let (world_pt, mut d2) = match tg {
|
let (world_pt, mut d2) = match tg {
|
||||||
TangentGeom::Line { p1, p2 } => {
|
TangentGeom::Line { p1, p2 } => {
|
||||||
let (lp1, lp2) = tangent_line_endpoints(wire, *p1, *p2, &mut line_cursor);
|
let (lp1, lp2) =
|
||||||
|
tangent_line_endpoints(wire, *p1, *p2, &mut line_cursor);
|
||||||
let sp0 = world_to_screen(lp1, view_rot, eye, bounds);
|
let sp0 = world_to_screen(lp1, view_rot, eye, bounds);
|
||||||
let sp1 = world_to_screen(lp2, view_rot, eye, bounds);
|
let sp1 = world_to_screen(lp2, view_rot, eye, bounds);
|
||||||
let d2 = dist2_to_segment(cursor_screen, sp0, sp1);
|
let d2 = dist2_to_segment(cursor_screen, sp0, sp1);
|
||||||
@@ -2481,11 +2483,9 @@ impl Snapper {
|
|||||||
let tangent_obj = match tg {
|
let tangent_obj = match tg {
|
||||||
TangentGeom::Line { p1, p2 } => {
|
TangentGeom::Line { p1, p2 } => {
|
||||||
let mut cursor = 0usize;
|
let mut cursor = 0usize;
|
||||||
let (lp1, lp2) = tangent_line_endpoints(wire, *p1, *p2, &mut cursor);
|
let (lp1, lp2) =
|
||||||
TangentObject::Line {
|
tangent_line_endpoints(wire, *p1, *p2, &mut cursor);
|
||||||
p1: lp1,
|
TangentObject::Line { p1: lp1, p2: lp2 }
|
||||||
p2: lp2,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
TangentGeom::Circle { center, radius } => TangentObject::Circle {
|
TangentGeom::Circle { center, radius } => TangentObject::Circle {
|
||||||
center: glam::DVec3::new(
|
center: glam::DVec3::new(
|
||||||
@@ -5649,49 +5649,160 @@ mod ext_tests {
|
|||||||
use super::{parse_snap_override, SnapOverrideKind, SnapType};
|
use super::{parse_snap_override, SnapOverrideKind, SnapType};
|
||||||
|
|
||||||
// 3-letter prefixes
|
// 3-letter prefixes
|
||||||
assert_eq!(parse_snap_override("end"), Some(SnapOverrideKind::Mode(SnapType::Endpoint)));
|
assert_eq!(
|
||||||
assert_eq!(parse_snap_override("mid"), Some(SnapOverrideKind::Mode(SnapType::Midpoint)));
|
parse_snap_override("end"),
|
||||||
assert_eq!(parse_snap_override("cen"), Some(SnapOverrideKind::Mode(SnapType::Center)));
|
Some(SnapOverrideKind::Mode(SnapType::Endpoint))
|
||||||
assert_eq!(parse_snap_override("nod"), Some(SnapOverrideKind::Mode(SnapType::Node)));
|
);
|
||||||
assert_eq!(parse_snap_override("qua"), Some(SnapOverrideKind::Mode(SnapType::Quadrant)));
|
assert_eq!(
|
||||||
assert_eq!(parse_snap_override("int"), Some(SnapOverrideKind::Mode(SnapType::Intersection)));
|
parse_snap_override("mid"),
|
||||||
assert_eq!(parse_snap_override("ext"), Some(SnapOverrideKind::Mode(SnapType::Extension)));
|
Some(SnapOverrideKind::Mode(SnapType::Midpoint))
|
||||||
assert_eq!(parse_snap_override("ins"), Some(SnapOverrideKind::Mode(SnapType::Insertion)));
|
);
|
||||||
assert_eq!(parse_snap_override("per"), Some(SnapOverrideKind::Mode(SnapType::Perpendicular)));
|
assert_eq!(
|
||||||
assert_eq!(parse_snap_override("tan"), Some(SnapOverrideKind::Mode(SnapType::Tangent)));
|
parse_snap_override("cen"),
|
||||||
assert_eq!(parse_snap_override("nea"), Some(SnapOverrideKind::Mode(SnapType::Nearest)));
|
Some(SnapOverrideKind::Mode(SnapType::Center))
|
||||||
assert_eq!(parse_snap_override("app"), Some(SnapOverrideKind::Mode(SnapType::ApparentIntersection)));
|
);
|
||||||
assert_eq!(parse_snap_override("par"), Some(SnapOverrideKind::Mode(SnapType::Parallel)));
|
assert_eq!(
|
||||||
|
parse_snap_override("nod"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Node))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("qua"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Quadrant))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("int"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Intersection))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("ext"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Extension))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("ins"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Insertion))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("per"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Perpendicular))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("tan"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Tangent))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("nea"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Nearest))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("app"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::ApparentIntersection))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("par"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Parallel))
|
||||||
|
);
|
||||||
assert_eq!(parse_snap_override("non"), Some(SnapOverrideKind::None));
|
assert_eq!(parse_snap_override("non"), Some(SnapOverrideKind::None));
|
||||||
assert_eq!(parse_snap_override("geo"), Some(SnapOverrideKind::Mode(SnapType::GeometricCenter)));
|
assert_eq!(
|
||||||
assert_eq!(parse_snap_override("gce"), Some(SnapOverrideKind::Mode(SnapType::GeometricCenter)));
|
parse_snap_override("geo"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::GeometricCenter))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("gce"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::GeometricCenter))
|
||||||
|
);
|
||||||
|
|
||||||
// Case insensitivity & leading underscores
|
// Case insensitivity & leading underscores
|
||||||
assert_eq!(parse_snap_override("_MID"), Some(SnapOverrideKind::Mode(SnapType::Midpoint)));
|
assert_eq!(
|
||||||
assert_eq!(parse_snap_override("_Endpoint"), Some(SnapOverrideKind::Mode(SnapType::Endpoint)));
|
parse_snap_override("_MID"),
|
||||||
assert_eq!(parse_snap_override(" _NON "), Some(SnapOverrideKind::None));
|
Some(SnapOverrideKind::Mode(SnapType::Midpoint))
|
||||||
assert_eq!(parse_snap_override("CEN"), Some(SnapOverrideKind::Mode(SnapType::Center)));
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("_Endpoint"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Endpoint))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override(" _NON "),
|
||||||
|
Some(SnapOverrideKind::None)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("CEN"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Center))
|
||||||
|
);
|
||||||
|
|
||||||
// Full names and aliases
|
// Full names and aliases
|
||||||
assert_eq!(parse_snap_override("endpoint"), Some(SnapOverrideKind::Mode(SnapType::Endpoint)));
|
assert_eq!(
|
||||||
assert_eq!(parse_snap_override("midpoint"), Some(SnapOverrideKind::Mode(SnapType::Midpoint)));
|
parse_snap_override("endpoint"),
|
||||||
assert_eq!(parse_snap_override("center"), Some(SnapOverrideKind::Mode(SnapType::Center)));
|
Some(SnapOverrideKind::Mode(SnapType::Endpoint))
|
||||||
assert_eq!(parse_snap_override("centre"), Some(SnapOverrideKind::Mode(SnapType::Center)));
|
);
|
||||||
assert_eq!(parse_snap_override("quadrant"), Some(SnapOverrideKind::Mode(SnapType::Quadrant)));
|
assert_eq!(
|
||||||
assert_eq!(parse_snap_override("intersection"), Some(SnapOverrideKind::Mode(SnapType::Intersection)));
|
parse_snap_override("midpoint"),
|
||||||
assert_eq!(parse_snap_override("perpendicular"), Some(SnapOverrideKind::Mode(SnapType::Perpendicular)));
|
Some(SnapOverrideKind::Mode(SnapType::Midpoint))
|
||||||
assert_eq!(parse_snap_override("tangent"), Some(SnapOverrideKind::Mode(SnapType::Tangent)));
|
);
|
||||||
assert_eq!(parse_snap_override("nearest"), Some(SnapOverrideKind::Mode(SnapType::Nearest)));
|
assert_eq!(
|
||||||
assert_eq!(parse_snap_override("apparent"), Some(SnapOverrideKind::Mode(SnapType::ApparentIntersection)));
|
parse_snap_override("center"),
|
||||||
assert_eq!(parse_snap_override("apparentintersection"), Some(SnapOverrideKind::Mode(SnapType::ApparentIntersection)));
|
Some(SnapOverrideKind::Mode(SnapType::Center))
|
||||||
assert_eq!(parse_snap_override("appint"), Some(SnapOverrideKind::Mode(SnapType::ApparentIntersection)));
|
);
|
||||||
assert_eq!(parse_snap_override("parallel"), Some(SnapOverrideKind::Mode(SnapType::Parallel)));
|
assert_eq!(
|
||||||
assert_eq!(parse_snap_override("insert"), Some(SnapOverrideKind::Mode(SnapType::Insertion)));
|
parse_snap_override("centre"),
|
||||||
assert_eq!(parse_snap_override("insertion"), Some(SnapOverrideKind::Mode(SnapType::Insertion)));
|
Some(SnapOverrideKind::Mode(SnapType::Center))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("quadrant"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Quadrant))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("intersection"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Intersection))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("perpendicular"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Perpendicular))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("tangent"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Tangent))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("nearest"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Nearest))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("apparent"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::ApparentIntersection))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("apparentintersection"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::ApparentIntersection))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("appint"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::ApparentIntersection))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("parallel"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Parallel))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("insert"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Insertion))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("insertion"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::Insertion))
|
||||||
|
);
|
||||||
assert_eq!(parse_snap_override("none"), Some(SnapOverrideKind::None));
|
assert_eq!(parse_snap_override("none"), Some(SnapOverrideKind::None));
|
||||||
assert_eq!(parse_snap_override("gcen"), Some(SnapOverrideKind::Mode(SnapType::GeometricCenter)));
|
assert_eq!(
|
||||||
assert_eq!(parse_snap_override("geometriccenter"), Some(SnapOverrideKind::Mode(SnapType::GeometricCenter)));
|
parse_snap_override("gcen"),
|
||||||
assert_eq!(parse_snap_override("geocenter"), Some(SnapOverrideKind::Mode(SnapType::GeometricCenter)));
|
Some(SnapOverrideKind::Mode(SnapType::GeometricCenter))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("geometriccenter"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::GeometricCenter))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_snap_override("geocenter"),
|
||||||
|
Some(SnapOverrideKind::Mode(SnapType::GeometricCenter))
|
||||||
|
);
|
||||||
|
|
||||||
// Rejection of strings shorter than 3 chars
|
// Rejection of strings shorter than 3 chars
|
||||||
assert_eq!(parse_snap_override("en"), None);
|
assert_eq!(parse_snap_override("en"), None);
|
||||||
|
|||||||
+7
-4
@@ -743,10 +743,13 @@ pub fn download_bytes(name: &str, bytes: &[u8]) {
|
|||||||
pub fn save_hook() -> Option<js_sys::Function> {
|
pub fn save_hook() -> Option<js_sys::Function> {
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
let window = web_sys::window()?;
|
let window = web_sys::window()?;
|
||||||
js_sys::Reflect::get(window.as_ref(), &wasm_bindgen::JsValue::from_str("openCadStudioSaveFile"))
|
js_sys::Reflect::get(
|
||||||
.ok()?
|
window.as_ref(),
|
||||||
.dyn_into::<js_sys::Function>()
|
&wasm_bindgen::JsValue::from_str("openCadStudioSaveFile"),
|
||||||
.ok()
|
)
|
||||||
|
.ok()?
|
||||||
|
.dyn_into::<js_sys::Function>()
|
||||||
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show a plot preview as an in-page overlay. The plotted PDF arrives already
|
/// Show a plot preview as an in-page overlay. The plotted PDF arrives already
|
||||||
|
|||||||
@@ -553,7 +553,11 @@ impl CommandLine {
|
|||||||
let fade = self.fade_secs();
|
let fade = self.fade_secs();
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
for entry in &mut self.history {
|
for entry in &mut self.history {
|
||||||
if since.saturating_duration_since(entry.created_at).as_secs_f32() < fade {
|
if since
|
||||||
|
.saturating_duration_since(entry.created_at)
|
||||||
|
.as_secs_f32()
|
||||||
|
< fade
|
||||||
|
{
|
||||||
entry.created_at = now;
|
entry.created_at = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -156,7 +156,9 @@ impl PanelId {
|
|||||||
pub fn icon(self) -> &'static [u8] {
|
pub fn icon(self) -> &'static [u8] {
|
||||||
match self {
|
match self {
|
||||||
PanelId::Properties => include_bytes!("../../assets/icons/properties.svg"),
|
PanelId::Properties => include_bytes!("../../assets/icons/properties.svg"),
|
||||||
PanelId::GraphicAttributes => include_bytes!("../../assets/icons/graphic_attributes.svg"),
|
PanelId::GraphicAttributes => {
|
||||||
|
include_bytes!("../../assets/icons/graphic_attributes.svg")
|
||||||
|
}
|
||||||
PanelId::BlockPalette => include_bytes!("../../assets/icons/blocks/insert.svg"),
|
PanelId::BlockPalette => include_bytes!("../../assets/icons/blocks/insert.svg"),
|
||||||
PanelId::ExternalReferences => crate::ui::icons::FOLDER_OPEN,
|
PanelId::ExternalReferences => crate::ui::icons::FOLDER_OPEN,
|
||||||
PanelId::Browser => include_bytes!("../../assets/icons/content_browser.svg"),
|
PanelId::Browser => include_bytes!("../../assets/icons/content_browser.svg"),
|
||||||
|
|||||||
+1
-1
@@ -6,9 +6,9 @@ pub mod color_select;
|
|||||||
pub mod command_line;
|
pub mod command_line;
|
||||||
pub mod dock;
|
pub mod dock;
|
||||||
pub mod icons;
|
pub mod icons;
|
||||||
|
pub mod menu_bar;
|
||||||
pub mod modal;
|
pub mod modal;
|
||||||
pub mod node_graph;
|
pub mod node_graph;
|
||||||
pub mod menu_bar;
|
|
||||||
pub mod overlay;
|
pub mod overlay;
|
||||||
pub mod popup;
|
pub mod popup;
|
||||||
pub mod properties;
|
pub mod properties;
|
||||||
|
|||||||
Loaded 100 of 113 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user