Merge branch 'main' into feature/parametric-constraint-system

# Conflicts:
#	src/app/automation.rs
#	src/app/mod.rs
#	src/ui/ribbon/widgets.rs
This commit is contained in:
Felix Riestra committed 2026-09-09 13:35:07 +02:00
commit e2d39b049d
114 files changed
+13641 -1685

No files matched your search

+10
View File
@@ -18,3 +18,13 @@ rustflags = [
# WebAssembly stack while developing with `trunk serve`. # WebAssembly stack while developing with `trunk serve`.
'-Clink-arg=-zstack-size=4194304', '-Clink-arg=-zstack-size=4194304',
] ]
[target.x86_64-pc-windows-gnu]
rustflags = [
"-Clink-arg=-Wl,--stack,16777216",
]
[target.x86_64-pc-windows-msvc]
rustflags = [
"-Clink-arg=/STACK:16777216",
]
+1 -4
View File
@@ -398,10 +398,7 @@ jobs:
APP=OpenCADStudio.app APP=OpenCADStudio.app
rm -rf "$APP" rm -rf "$APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" "$APP/Contents/PlugIns" mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" "$APP/Contents/PlugIns"
# The launcher is CFBundleExecutable (Finder always launches it, # Finder opens the launcher, which relays files to the sibling GUI.
# for both document opens and plain launches); it execs into the
# real GUI binary, renamed alongside it, over argv. See
# src/bin/ocs_launcher.rs.
cp target/aarch64-apple-darwin/release/ocs_launcher "$APP/Contents/MacOS/OpenCADStudio" cp target/aarch64-apple-darwin/release/ocs_launcher "$APP/Contents/MacOS/OpenCADStudio"
cp target/aarch64-apple-darwin/release/OpenCADStudio "$APP/Contents/MacOS/OpenCADStudio-App" cp target/aarch64-apple-darwin/release/OpenCADStudio "$APP/Contents/MacOS/OpenCADStudio-App"
chmod +x "$APP/Contents/MacOS/OpenCADStudio" "$APP/Contents/MacOS/OpenCADStudio-App" chmod +x "$APP/Contents/MacOS/OpenCADStudio" "$APP/Contents/MacOS/OpenCADStudio-App"
Generated
+2 -2
View File
@@ -80,7 +80,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]] [[package]]
name = "acadrust" name = "acadrust"
version = "0.5.4" version = "0.5.4"
source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=5b56571a190e7a17c8f12d36390d2938b0fb72f7#5b56571a190e7a17c8f12d36390d2938b0fb72f7" source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=7b4c112bd7895fe9039514c21c5b5aa59eaf2525#7b4c112bd7895fe9039514c21c5b5aa59eaf2525"
dependencies = [ dependencies = [
"ahash 0.8.12", "ahash 0.8.12",
"anyhow", "anyhow",
@@ -895,7 +895,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]] [[package]]
name = "cadkernel" name = "cadkernel"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=6a5e6fc2822f6af6b2611cd605b44437b35158f0#6a5e6fc2822f6af6b2611cd605b44437b35158f0" source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=8efd43cb2f04577d20c3ee33ce5273bb49affa3d#8efd43cb2f04577d20c3ee33ce5273bb49affa3d"
dependencies = [ dependencies = [
"acadrust", "acadrust",
"cavalier_contours", "cavalier_contours",
+3 -6
View File
@@ -3,6 +3,7 @@ name = "OpenCADStudio"
version = "2026.36.0" version = "2026.36.0"
edition = "2021" edition = "2021"
build = "build.rs" build = "build.rs"
default-run = "OpenCADStudio"
[workspace] [workspace]
members = [ members = [
@@ -32,8 +33,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
rfd = "0.17" rfd = "0.17"
clap = { version = "4", features = ["derive", "string"] } clap = { version = "4", features = ["derive", "string"] }
env_logger = "0.11" env_logger = "0.11"
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b56571a190e7a17c8f12d36390d2938b0fb72f7", features = ["serde"] } acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7b4c112bd7895fe9039514c21c5b5aa59eaf2525", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6a5e6fc2822f6af6b2611cd605b44437b35158f0", features = ["acis", "offset"] } cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "8efd43cb2f04577d20c3ee33ce5273bb49affa3d", features = ["acis", "offset"] }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1" flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
@@ -74,10 +75,6 @@ objc2-foundation = { version = "0.2", features = ["NSArray", "NSURL", "NSString"
iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
[patch."https://github.com/HakanSeven12/cadcodec.git"]
acadrust = { git = "https://[email protected]/HakanSeven12/cadcodec.git", rev = "5b56571a190e7a17c8f12d36390d2938b0fb72f7" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
getrandom = "0.3" getrandom = "0.3"
ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] } ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] }
+1 -1
View File
@@ -259,7 +259,7 @@ pub unsafe extern "C" fn dwg_thumbnail_png(
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn dwg_thumbnail_free(ptr: *mut u8, len: usize) { pub unsafe extern "C" fn dwg_thumbnail_free(ptr: *mut u8, len: usize) {
if !ptr.is_null() && len != 0 { if !ptr.is_null() && len != 0 {
drop(Box::from_raw(std::slice::from_raw_parts_mut(ptr, len))); drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len)));
} }
} }
+2 -2
View File
@@ -14,8 +14,8 @@ bincode = "1.3"
thiserror = "1" thiserror = "1"
# Host adapters convert ObjectId to acadrust handles; kernel handles stay local. # Host adapters convert ObjectId to acadrust handles; kernel handles stay local.
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b56571a190e7a17c8f12d36390d2938b0fb72f7", features = ["serde"], optional = true } acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7b4c112bd7895fe9039514c21c5b5aa59eaf2525", features = ["serde"], optional = true }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6a5e6fc2822f6af6b2611cd605b44437b35158f0", features = ["acis", "offset"], optional = true } cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "8efd43cb2f04577d20c3ee33ce5273bb49affa3d", features = ["acis", "offset"], optional = true }
ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] } ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] }
[build-dependencies] [build-dependencies]
+1 -4
View File
@@ -450,10 +450,7 @@ fn profile_curves<B: DocApiBackend>(
b.profile_curves(profile) b.profile_curves(profile)
} }
/// Validate a `Curve2Spec`'s fallible conditions BEFORE mutation (the same rules /// Validate that an entity exists before performing an operation.
/// the host's `curve_spec_to_entity` enforces), so `CreateMany` stays all-or-nothing.
/// Currently: ellipse minor/major ratio must be in (0, 1].
fn require_exists<B: DocApiBackend>(b: &B, id: ObjectId, op: &'static str) -> ApiResult<()> { fn require_exists<B: DocApiBackend>(b: &B, id: ObjectId, op: &'static str) -> ApiResult<()> {
if b.entity_exists(id) { if b.entity_exists(id) {
Ok(()) Ok(())
+1 -1
View File
@@ -219,7 +219,7 @@ Broadcasting is V4-gated inside `PluginManager::broadcast_notification`: V2/V3 p
The crate embeds two JSON blobs at build time: The crate embeds two JSON blobs at build time:
- **Type registry** (`OUT_DIR/type_registry.json`) — a language-binding-friendly schema for a curated allow-list of `acadrust` types, generated by `serde-reflection` in `build.rs`. See [`src/type_registry.rs`](src/type_registry.rs). - **Type registry** (`OUT_DIR/type_registry.json`) — a language-binding-friendly schema for every editor record root and its dependent types, generated by `serde-reflection` in `build.rs`. See [`src/type_registry.rs`](src/type_registry.rs).
- **Version info** (`OUT_DIR/version_info.json`) — host version, `ocs_plugin_api` version, `acadrust` version and source, `rustc` version, API versions, build timestamp. See [`src/version_info.rs`](src/version_info.rs). - **Version info** (`OUT_DIR/version_info.json`) — host version, `ocs_plugin_api` version, `acadrust` version and source, `rustc` version, API versions, build timestamp. See [`src/version_info.rs`](src/version_info.rs).
Both are accessible without enabling the `host` feature. Both are accessible without enabling the `host` feature.
+2 -2
View File
@@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] }
# Pulled in only by the `host` feature, which adds the `acadrust`-typed # Pulled in only by the `host` feature, which adds the `acadrust`-typed
# `HostApi` runtime surface. The default crate stays dependency-free so engine # `HostApi` runtime surface. The default crate stays dependency-free so engine
# crates and external tooling can depend on the manifest/ribbon contract cheaply. # crates and external tooling can depend on the manifest/ribbon contract cheaply.
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c91a1c1de89a0fbc006dc360ddb0251ac8c207d1", optional = true, features = ["serde"] } acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7b4c112bd7895fe9039514c21c5b5aa59eaf2525", optional = true, features = ["serde"] }
# Runtime IPC and serialization (host feature only). # Runtime IPC and serialization (host feature only).
interprocess = { version = "2", optional = true } interprocess = { version = "2", optional = true }
@@ -38,7 +38,7 @@ serde_json = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
cargo-lock = "11" cargo-lock = "11"
# acadrust is scanned at build time to generate the embedded type registry. # acadrust is scanned at build time to generate the embedded type registry.
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c91a1c1de89a0fbc006dc360ddb0251ac8c207d1", features = ["serde"] } acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7b4c112bd7895fe9039514c21c5b5aa59eaf2525", features = ["serde"] }
[dev-dependencies] [dev-dependencies]
serde_json = "1" serde_json = "1"
+1 -1
View File
@@ -243,7 +243,7 @@ session belongs to.
`ocs_plugin_api` embeds two JSON blobs at build time: `ocs_plugin_api` embeds two JSON blobs at build time:
- **Type registry** — a stable, language-binding-friendly schema generated by - **Type registry** — a stable, language-binding-friendly schema generated by
tracing a curated allow-list of `acadrust` types with `serde-reflection`. tracing every editor record root and its dependent types with `serde-reflection`.
- **Version info** — `ocs_version`, `ocs_plugin_api_version`, `acadrust_version`, - **Version info** — `ocs_version`, `ocs_plugin_api_version`, `acadrust_version`,
`api_version`, etc. `api_version`, etc.
+404 -5
View File
@@ -34,11 +34,42 @@ fn generate_type_registry(out_dir: &Path) {
let mut samples = Samples::new(); let mut samples = Samples::new();
add_enum_samples(&mut tracer, &mut samples); add_enum_samples(&mut tracer, &mut samples);
// Curated allow-list of acadrust types. We intentionally exclude // Trace the complete record roots used by the editor and MCP API. Nested
// EntityType here to avoid pulling in all 41+ entity variants and any // entity/object variants and their enums are added to the same registry.
// custom-serializer edge cases.
type TraceFn = fn(&mut Tracer, &Samples); type TraceFn = fn(&mut Tracer, &Samples);
let types: Vec<(&str, TraceFn)> = vec![ let types: Vec<(&str, TraceFn)> = vec![
("EntityType", trace::<acadrust::EntityType>),
("ObjectType", trace::<acadrust::objects::ObjectType>),
(
"HeaderVariables",
trace::<acadrust::document::HeaderVariables>,
),
("SummaryInfo", trace::<acadrust::document::SummaryInfo>),
("LineType", trace::<acadrust::LineType>),
("TextStyle", trace::<acadrust::TextStyle>),
("BlockRecord", trace::<acadrust::BlockRecord>),
("DimStyle", trace::<acadrust::DimStyle>),
("AppId", trace::<acadrust::AppId>),
("View", trace::<acadrust::View>),
("VPort", trace::<acadrust::VPort>),
("Ucs", trace::<acadrust::Ucs>),
("VxTableRecord", trace::<acadrust::VxTableRecord>),
("DxfClass", trace::<acadrust::classes::DxfClass>),
(
"BlockVisibilityParameter",
trace::<acadrust::objects::BlockVisibilityParameter>,
),
("FieldDef", trace::<acadrust::document::FieldDef>),
(
"DgnLsDefinition",
trace::<acadrust::objects::DgnLsDefinition>,
),
("DgnLsComponent", trace::<acadrust::objects::DgnLsComponent>),
(
"NotificationCollection",
trace::<acadrust::notification::NotificationCollection>,
),
("Preview", trace::<acadrust::document::Preview>),
("Point", trace::<acadrust::Point>), ("Point", trace::<acadrust::Point>),
("Line", trace::<acadrust::Line>), ("Line", trace::<acadrust::Line>),
("Circle", trace::<acadrust::Circle>), ("Circle", trace::<acadrust::Circle>),
@@ -93,10 +124,366 @@ fn generate_type_registry(out_dir: &Path) {
); );
} }
type TraceSimpleFn = fn(&mut Tracer);
let enums: Vec<(&str, TraceSimpleFn)> = vec![
(
"AssocAnnotationKind",
trace_simple::<acadrust::objects::AssocAnnotationKind>,
),
(
"AssocConstraintNodeData",
trace_simple::<acadrust::objects::AssocConstraintNodeData>,
),
(
"AssocEvalValue",
trace_simple::<acadrust::objects::AssocEvalValue>,
),
(
"AssocSubcurveKind",
trace_simple::<acadrust::objects::AssocSubcurveKind>,
),
(
"AssocSurfaceActionKind",
trace_simple::<acadrust::objects::AssocSurfaceActionKind>,
),
(
"AssocViewObjectActionParamKind",
trace_simple::<acadrust::objects::AssocViewObjectActionParamKind>,
),
(
"AcisVersion",
trace_simple::<acadrust::entities::AcisVersion>,
),
(
"AssociativeData",
trace_simple::<acadrust::objects::AssociativeData>,
),
(
"AttachmentPointType",
trace_simple::<acadrust::entities::AttachmentPointType>,
),
(
"BlockContentConnectionType",
trace_simple::<acadrust::entities::BlockContentConnectionType>,
),
(
"BlockEvalValue",
trace_simple::<acadrust::objects::BlockEvalValue>,
),
("BorderType", trace_simple::<acadrust::entities::BorderType>),
(
"BoundaryEdge",
trace_simple::<acadrust::entities::BoundaryEdge>,
),
(
"BreakFlowDirection",
trace_simple::<acadrust::entities::BreakFlowDirection>,
),
(
"CellAlignment",
trace_simple::<acadrust::objects::CellAlignment>,
),
(
"CellStyleType",
trace_simple::<acadrust::entities::CellStyleType>,
),
("CellType", trace_simple::<acadrust::entities::CellType>),
(
"CellValueType",
trace_simple::<acadrust::entities::CellValueType>,
),
(
"ClassObjectData",
trace_simple::<acadrust::objects::ClassObjectData>,
),
("ClipMode", trace_simple::<acadrust::entities::ClipMode>),
("ClipType", trace_simple::<acadrust::entities::ClipType>),
(
"CompoundEntry",
trace_simple::<acadrust::compound_file::CompoundEntry>,
),
(
"CompoundPropertyValue",
trace_simple::<acadrust::compound_file::CompoundPropertyValue>,
),
(
"CompoundStreamContent",
trace_simple::<acadrust::compound_file::CompoundStreamContent>,
),
(
"DataObjectData",
trace_simple::<acadrust::objects::DataObjectData>,
),
(
"DgnLineStyleData",
trace_simple::<acadrust::objects::DgnLineStyleData>,
),
(
"DgnLsComponentData",
trace_simple::<acadrust::objects::DgnLsComponentData>,
),
(
"DgnLsComponentType",
trace_simple::<acadrust::objects::DgnLsComponentType>,
),
(
"DgnLsPhaseMode",
trace_simple::<acadrust::objects::DgnLsPhaseMode>,
),
("DimSubtype", trace_simple::<acadrust::objects::DimSubtype>),
("Dimension", trace_simple::<acadrust::entities::Dimension>),
(
"DimensionType",
trace_simple::<acadrust::entities::DimensionType>,
),
(
"DynamicBlockData",
trace_simple::<acadrust::objects::DynamicBlockData>,
),
(
"EmbeddedEntity",
trace_simple::<acadrust::entities::EmbeddedEntity>,
),
(
"ExtendedEntityData",
trace_simple::<acadrust::entities::ExtendedEntityData>,
),
(
"FlowDirectionType",
trace_simple::<acadrust::entities::FlowDirectionType>,
),
(
"HatchPatternType",
trace_simple::<acadrust::entities::HatchPatternType>,
),
(
"HatchStyleType",
trace_simple::<acadrust::entities::HatchStyleType>,
),
(
"HelixConstraint",
trace_simple::<acadrust::entities::HelixConstraint>,
),
(
"HooklineDirection",
trace_simple::<acadrust::entities::HooklineDirection>,
),
(
"HorizontalAlignment",
trace_simple::<acadrust::entities::HorizontalAlignment>,
),
(
"LeaderContentType",
trace_simple::<acadrust::entities::LeaderContentType>,
),
(
"LeaderCreationType",
trace_simple::<acadrust::entities::LeaderCreationType>,
),
(
"LeaderDrawOrderType",
trace_simple::<acadrust::objects::LeaderDrawOrderType>,
),
(
"LeaderPathType",
trace_simple::<acadrust::entities::LeaderPathType>,
),
(
"LineTypeComplexContent",
trace_simple::<acadrust::tables::LineTypeComplexContent>,
),
(
"LegacyEntityData",
trace_simple::<acadrust::entities::LegacyEntityData>,
),
(
"MLineJustification",
trace_simple::<acadrust::entities::MLineJustification>,
),
("MTextFlag", trace_simple::<acadrust::entities::MTextFlag>),
(
"MaterialProceduralValue",
trace_simple::<acadrust::objects::MaterialProceduralValue>,
),
(
"MultiLeaderDrawOrderType",
trace_simple::<acadrust::objects::MultiLeaderDrawOrderType>,
),
(
"MultiLeaderPathType",
trace_simple::<acadrust::entities::MultiLeaderPathType>,
),
(
"ObjectContextKind",
trace_simple::<acadrust::objects::ObjectContextKind>,
),
(
"NotificationType",
trace_simple::<acadrust::notification::NotificationType>,
),
(
"OleFrameEnvelope",
trace_simple::<acadrust::entities::OleFrameEnvelope>,
),
(
"OleObjectType",
trace_simple::<acadrust::entities::OleObjectType>,
),
(
"PlotPaperUnits",
trace_simple::<acadrust::objects::PlotPaperUnits>,
),
(
"PlotRotation",
trace_simple::<acadrust::objects::PlotRotation>,
),
("PlotType", trace_simple::<acadrust::objects::PlotType>),
(
"PreviewFormat",
trace_simple::<acadrust::document::PreviewFormat>,
),
(
"PolyfaceSmoothType",
trace_simple::<acadrust::entities::PolyfaceSmoothType>,
),
(
"ProxyPayloadEncoding",
trace_simple::<acadrust::objects::ProxyPayloadEncoding>,
),
(
"ResolutionUnit",
trace_simple::<acadrust::objects::ResolutionUnit>,
),
("ScaledType", trace_simple::<acadrust::objects::ScaledType>),
(
"SemanticPropertyValue",
trace_simple::<acadrust::objects::SemanticPropertyValue>,
),
(
"ShadePlotMode",
trace_simple::<acadrust::objects::ShadePlotMode>,
),
(
"ShadePlotResolutionLevel",
trace_simple::<acadrust::objects::ShadePlotResolutionLevel>,
),
(
"SolidHistoryOperation",
trace_simple::<acadrust::objects::SolidHistoryOperation>,
),
(
"SurfaceData",
trace_simple::<acadrust::entities::SurfaceData>,
),
(
"SurfaceKind",
trace_simple::<acadrust::entities::SurfaceKind>,
),
(
"SurfaceSmoothType",
trace_simple::<acadrust::entities::SurfaceSmoothType>,
),
(
"TableBorderType",
trace_simple::<acadrust::objects::TableBorderType>,
),
(
"TableCellContentType",
trace_simple::<acadrust::entities::TableCellContentType>,
),
(
"TableFlowDirection",
trace_simple::<acadrust::objects::TableFlowDirection>,
),
(
"TextAlignmentType",
trace_simple::<acadrust::entities::TextAlignmentType>,
),
(
"TextAngleType",
trace_simple::<acadrust::entities::TextAngleType>,
),
(
"TextAttachmentDirectionType",
trace_simple::<acadrust::entities::TextAttachmentDirectionType>,
),
(
"TextAttachmentPointType",
trace_simple::<acadrust::entities::TextAttachmentPointType>,
),
(
"TextAttachmentType",
trace_simple::<acadrust::entities::TextAttachmentType>,
),
(
"TextHorizontalAlignment",
trace_simple::<acadrust::entities::TextHorizontalAlignment>,
),
(
"TextVerticalAlignment",
trace_simple::<acadrust::entities::TextVerticalAlignment>,
),
(
"UnderlayType",
trace_simple::<acadrust::entities::UnderlayType>,
),
(
"ValueUnitType",
trace_simple::<acadrust::entities::ValueUnitType>,
),
(
"VbaDirectoryValue",
trace_simple::<acadrust::vba::VbaDirectoryValue>,
),
(
"VerticalAlignment",
trace_simple::<acadrust::entities::VerticalAlignment>,
),
(
"ViewportRenderMode",
trace_simple::<acadrust::entities::ViewportRenderMode>,
),
(
"VisualStylePropertyValue",
trace_simple::<acadrust::objects::VisualStylePropertyValue>,
),
(
"ViewRepSketchGeometry",
trace_simple::<acadrust::objects::ViewRepSketchGeometry>,
),
(
"WipeoutClipMode",
trace_simple::<acadrust::entities::WipeoutClipMode>,
),
(
"WipeoutClipType",
trace_simple::<acadrust::entities::WipeoutClipType>,
),
("WireType", trace_simple::<acadrust::entities::WireType>),
];
for (name, trace) in enums {
trace(&mut tracer);
eprintln!("[ocs_plugin_api build] traced enum variants: {name}");
}
let traced = tracer let traced = tracer
.registry() .registry()
.expect("type registry tracing failed; see stderr for individual errors"); .expect("type registry tracing failed; see stderr for individual errors");
let registry = map_to_custom_schema(&traced); let mut registry = map_to_custom_schema(&traced);
let mut section_style_tracer = Tracer::new(TracerConfig::default());
section_style_tracer
.trace_simple_type::<acadrust::entities::SectionViewStyle>()
.expect("section view style tracing failed");
let section_style_registry = section_style_tracer
.registry()
.expect("section view style registry failed");
let section_style = section_style_registry
.get("SectionViewStyle")
.expect("section view style registry entry");
registry.types.insert(
TypeId::new("EntitySectionViewStyle"),
map_container("EntitySectionViewStyle", section_style),
);
let json = serde_json::to_string_pretty(&registry).unwrap(); let json = serde_json::to_string_pretty(&registry).unwrap();
fs::write(out_dir.join("type_registry.json"), json).unwrap(); fs::write(out_dir.join("type_registry.json"), json).unwrap();
} }
@@ -114,10 +501,22 @@ where
} }
} }
fn trace_simple<T>(tracer: &mut Tracer)
where
T: DeserializeOwned,
{
if let Err(error) = tracer.trace_simple_type::<T>() {
panic!(
"type registry enum tracing failed for {}: {error}",
std::any::type_name::<T>()
);
}
}
fn add_enum_samples(tracer: &mut Tracer, samples: &mut Samples) { fn add_enum_samples(tracer: &mut Tracer, samples: &mut Samples) {
// serde-reflection needs at least one sample value per enum variant in // serde-reflection needs at least one sample value per enum variant in
// order to reconstruct the full schema. Provide samples for the enums that // order to reconstruct the full schema. Provide samples for the enums that
// appear inside the allow-list types below. // need concrete serialized values.
let _ = tracer.trace_value(samples, &acadrust::LineWeight::ByLayer); let _ = tracer.trace_value(samples, &acadrust::LineWeight::ByLayer);
let _ = tracer.trace_value(samples, &acadrust::LineWeight::ByBlock); let _ = tracer.trace_value(samples, &acadrust::LineWeight::ByBlock);
let _ = tracer.trace_value(samples, &acadrust::LineWeight::Default); let _ = tracer.trace_value(samples, &acadrust::LineWeight::Default);
+5 -2
View File
@@ -746,10 +746,13 @@ mod repl {
error: Option<String>, error: Option<String>,
error_type: Option<String>, error_type: Option<String>,
traceback: Option<String>, traceback: Option<String>,
line_number: Option<u32>, position: Option<(u32, u32)>,
column_number: Option<u32>,
duration_ms: f64, duration_ms: f64,
) -> Self { ) -> Self {
let (line_number, column_number) = match position {
Some((l, c)) => (Some(l), Some(c)),
None => (None, None),
};
Self { Self {
success, success,
output, output,
+2 -9
View File
@@ -261,20 +261,13 @@ pub fn run_request_proxy(
token: [u8; PROXY_TOKEN_LEN], token: [u8; PROXY_TOKEN_LEN],
) -> std::io::Result<()> { ) -> std::io::Result<()> {
for stream in listener.incoming() { for stream in listener.incoming() {
let mut stream = match stream { let mut stream = stream?;
Ok(s) => s,
Err(e) => return Err(e),
};
if !verify_token(&mut stream, &token) { if !verify_token(&mut stream, &token) {
continue; continue;
} }
let sender = Arc::clone(&sender); let sender = Arc::clone(&sender);
std::thread::spawn(move || { std::thread::spawn(move || {
loop { while let Ok(req) = recv_framed(&mut stream) {
let req: PluginRequest = match recv_framed(&mut stream) {
Ok(r) => r,
Err(_) => break,
};
let resp = match sender.request(req) { let resp = match sender.request(req) {
Ok(r) => r, Ok(r) => r,
Err(e) => PluginResponse::Error(e.to_string()), Err(e) => PluginResponse::Error(e.to_string()),
+6 -4
View File
@@ -75,10 +75,12 @@ fn request_kind(req: &HostRequest) -> &'static str {
} }
} }
type DeferredRequest = (u64, Option<u64>, Box<PluginRequest>);
pub(crate) struct V4Connection { pub(crate) struct V4Connection {
shared: Arc<V4HostShared>, shared: Arc<V4HostShared>,
incoming: Mutex<mpsc::Receiver<HostIncoming>>, incoming: Mutex<mpsc::Receiver<HostIncoming>>,
deferred: Mutex<VecDeque<(u64, Option<u64>, Box<PluginRequest>)>>, deferred: Mutex<VecDeque<DeferredRequest>>,
call_lock: Mutex<()>, call_lock: Mutex<()>,
next_id: AtomicU64, next_id: AtomicU64,
reader_handle: Mutex<Option<std::thread::JoinHandle<()>>>, reader_handle: Mutex<Option<std::thread::JoinHandle<()>>>,
@@ -199,7 +201,7 @@ impl V4Connection {
tab_id, tab_id,
payload, payload,
}) => { }) => {
if tab_id.map_or(true, |request_tab| request_tab == host.tab_id()) { if tab_id.is_none_or(|request_tab| request_tab == host.tab_id()) {
self.respond_to_plugin_request( self.respond_to_plugin_request(
host, host,
rid, rid,
@@ -251,7 +253,7 @@ impl V4Connection {
let mut deferred = self.deferred.lock().unwrap_or_else(|e| e.into_inner()); let mut deferred = self.deferred.lock().unwrap_or_else(|e| e.into_inner());
let mut waiting = VecDeque::new(); let mut waiting = VecDeque::new();
while let Some((id, tab_id, payload)) = deferred.pop_front() { while let Some((id, tab_id, payload)) = deferred.pop_front() {
if tab_id.map_or(true, |request_tab| request_tab == current_tab_id) { if tab_id.is_none_or(|request_tab| request_tab == current_tab_id) {
ready.push_back((id, payload)); ready.push_back((id, payload));
} else { } else {
waiting.push_back((id, tab_id, payload)); waiting.push_back((id, tab_id, payload));
@@ -271,7 +273,7 @@ impl V4Connection {
tab_id, tab_id,
payload, payload,
}) => { }) => {
if tab_id.map_or(true, |request_tab| request_tab == current_tab_id) { if tab_id.is_none_or(|request_tab| request_tab == current_tab_id) {
self.respond_to_plugin_request( self.respond_to_plugin_request(
host, host,
id, id,
+43 -13
View File
@@ -1,17 +1,15 @@
//! Embedded type registry generated at build time. //! Embedded type registry generated at build time.
//! //!
//! The JSON embedded here is produced by tracing a curated allow-list of //! The JSON embedded here is produced by tracing every editor record root and
//! `acadrust` types with `serde-reflection` and mapping the result into a //! its dependent `acadrust` types with `serde-reflection`, then mapping the
//! stable, language-binding-friendly schema defined in //! result into a stable, language-binding-friendly schema defined in
//! [`crate::type_registry_types`]. The registry is embedded via //! [`crate::type_registry_types`]. The registry is embedded via
//! `include_str!(concat!(env!("OUT_DIR"), "/type_registry.json"))` so the //! `include_str!(concat!(env!("OUT_DIR"), "/type_registry.json"))` so the
//! `host` feature is not required to read it. //! `host` feature is not required to read it.
//! //!
//! The allow-list is intentionally narrow: it excludes generic containers and //! The registry covers the complete entity, object, symbol-table, header, and
//! internal enums such as `EntityType` in favor of concrete serializable structs //! document-record roots used by the editor. Language bindings and tooling can
//! like `Point`, `Line`, `Circle`, and `MText`. Language bindings and tooling //! inspect every variant without pulling the full `acadrust` dependency tree.
//! can depend on this schema without pulling the full `acadrust` dependency
//! tree.
pub use crate::type_registry_types::*; pub use crate::type_registry_types::*;
@@ -121,13 +119,45 @@ mod tests {
} }
#[test] #[test]
fn embedded_type_registry_does_not_contain_entity_type() { fn embedded_type_registry_contains_every_entity_and_object_variant() {
let registry: TypeRegistry = serde_json::from_str(get_embedded_type_registry_json()) let registry: TypeRegistry = serde_json::from_str(get_embedded_type_registry_json())
.expect("valid TypeRegistry JSON"); .expect("valid TypeRegistry JSON");
assert!( let entities = registry
!registry.types.contains_key(&TypeId::new("EntityType")), .types
"EntityType should not be in the allow-list registry" .get(&TypeId::new("EntityType"))
); .expect("EntityType in complete registry");
let objects = registry
.types
.get(&TypeId::new("ObjectType"))
.expect("ObjectType in complete registry");
assert_eq!(entities.variants.len(), 48);
assert_eq!(objects.variants.len(), 36);
for root in [
"HeaderVariables",
"SummaryInfo",
"LineType",
"TextStyle",
"BlockRecord",
"DimStyle",
"AppId",
"View",
"VPort",
"Ucs",
"VxTableRecord",
"DxfClass",
"BlockVisibilityParameter",
"FieldDef",
"DgnLsDefinition",
"DgnLsComponent",
"NotificationCollection",
"Preview",
"EntitySectionViewStyle",
] {
assert!(
registry.types.contains_key(&TypeId::new(root)),
"missing {root}"
);
}
} }
#[test] #[test]
+1 -1
View File
@@ -8,7 +8,7 @@ publish = false
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c91a1c1de89a0fbc006dc360ddb0251ac8c207d1", features = ["serde"] } acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7b4c112bd7895fe9039514c21c5b5aa59eaf2525", features = ["serde"] }
bincode = "1.3" bincode = "1.3"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
console_error_panic_hook = "0.1" console_error_panic_hook = "0.1"
+1 -1
View File
@@ -10,4 +10,4 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
ocs_plugin_api = { path = "../ocs_plugin_api", features = ["host"] } ocs_plugin_api = { path = "../ocs_plugin_api", features = ["host"] }
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "6dcda1a", features = ["serde"] } acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7b4c112bd7895fe9039514c21c5b5aa59eaf2525", features = ["serde"] }
+67 -8
View File
@@ -13,15 +13,15 @@ The client must start that command over standard input/output. If it asks for th
The server provides four tools: The server provides four tools:
- `ocs_sessions` finds running editor sessions and opens OpenCADStudio when none exists. - `ocs_sessions` finds running editor sessions and opens OpenCADStudio when none exists.
- `ocs_read` reads document state, command manifests, entities, layers, properties, kernel measurements and spatial relationships, history, events, and operation status. - `ocs_read` discovers capabilities and reads document state, complete database records, command manifests, entities, properties, kernel measurements and spatial relationships, history, events, and operation status.
- `ocs_execute` performs one operation or a sequential batch against the real editor. - `ocs_execute` performs one operation, an atomic record update, or a sequential batch against the real editor.
- `ocs_capture` returns a bounded PNG of the drawing viewport or complete window. - `ocs_capture` returns a bounded PNG of the drawing viewport or complete window.
The normal flow is to call `ocs_sessions`, read the chosen session with `ocs_read`, then pass the returned document state into `ocs_execute`. For example, an undo request has this shape: The normal flow is to call `ocs_sessions`, pass its returned `session_id` as `ocs_session_id` to the other tools, read the chosen session with `ocs_read`, then pass the returned document state into `ocs_execute`. For example, an undo request has this shape:
```json ```json
{ {
"session_id": "SESSION_FROM_OCS_SESSIONS", "ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
"request": { "request": {
"op": "undo", "op": "undo",
"request_id": "undo-1", "request_id": "undo-1",
@@ -31,13 +31,72 @@ The normal flow is to call `ocs_sessions`, read the chosen session with `ocs_rea
} }
``` ```
Call `ocs_read` with `op: "capabilities"` before unfamiliar work. It reports the command, geometry, transaction, capture, and database facilities supported by the running build. The `records.collections` list gives every available database collection, its record count, and whether it can be edited.
Call `ocs_read` with `op: "record_schema"` before editing an unfamiliar record. With no parameters it lists the complete generated type registry. A collection returns the record types accepted by that collection even when the current drawing has no instance of a type. Supplying both `collection` and `type` returns the type's complete dependency graph, flattened property paths, JSON types, optional and sequence markers, enum variants, integer bounds, unambiguous unit annotations, identity fields, and write rules:
```json
{
"ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
"op": "record_schema",
"parameters": {
"collection": "entities",
"type": "Insert"
}
}
```
Sequence fields include an `item_path_template` such as `/attributes/{index}`. Replace `{index}` with an index returned by `records` before using the path in `set_properties`. When a matching record exists, `runtime_schema` adds its exact JSON shape and bounded scalar examples. The embedded registry is generated from all 48 entity variants, all 36 non-graphical object variants, symbol-table records, header data, summary data, and the remaining stored record roots at build time.
`op: "records"` returns every serializable property of entities, non-graphical objects, symbol-table records, the complete header, document metadata, and derived database views. Omitting `collection` returns the same collection manifest; use `collection: "all"` for a paged query across the complete database. Records have a stable collection plus a handle or name, a type, and `properties`. Entity records also include their display and file record type names.
Property filters and projections use RFC 6901 JSON Pointer paths relative to `properties`. Multiple `where` filters are combined with AND. Supported operators are `eq`, `ne`, `lt`, `lte`, `gt`, `gte`, `contains`, `starts_with`, `ends_with`, `in`, `exists`, and `not_exists`:
```json
{
"ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
"op": "records",
"parameters": {
"collection": "entities",
"type": "Insert",
"where": [
{"path": "/common/layer", "value": "Equipment"},
{"path": "/attributes/0/tag", "value": "COMPANY"}
],
"paths": ["/block_name", "/insert_point", "/attributes"]
}
}
```
Use `set_properties` to replace one or more fields atomically. OCS verifies the record identity, JSON types, optional compare-and-set values, layer locks, document revision, and request ID before committing one undoable edit. Handles, ownership slots, table names, and other database identity fields are read-only; their dedicated editor commands preserve cross-record references.
```json
{
"ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
"request": {
"op": "set_properties",
"request_id": "attributes-1",
"document_id": 1,
"revision": 12,
"collection": "entities",
"handle": "2A",
"updates": [
{"path": "/attributes/0/value", "expected": "BPH", "value": "Updated"},
{"path": "/insert_point/z", "value": 3.0}
]
}
}
```
The same operation edits entries in `objects`, every symbol table, `header`, and `summary_info`. Collections marked `mutable: false` are derived or storage metadata and remain queryable. Their source records must be changed so OCS can rebuild those views consistently.
To discover command syntax without reading application source, call `ocs_read` with `op: "commands"`. The unfiltered response lists commands and actions. Add `parameters: {"name": "PLINE"}` for a command's batch examples and interactive guidance. To discover command syntax without reading application source, call `ocs_read` with `op: "commands"`. The unfiltered response lists commands and actions. Add `parameters: {"name": "PLINE"}` for a command's batch examples and interactive guidance.
A complete command can be sent with `run`. Prompt answers are separated by spaces, points use `x,y` or `x,y,z`, and option answers use their displayed token: A complete command can be sent with `run`. Prompt answers are separated by spaces, points use `x,y` or `x,y,z`, and option answers use their displayed token:
```json ```json
{ {
"session_id": "SESSION_FROM_OCS_SESSIONS", "ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
"request": { "request": {
"op": "run", "op": "run",
"request_id": "polyline-1", "request_id": "polyline-1",
@@ -52,7 +111,7 @@ Use `batch` when the steps are already known. OCS supplies each step with the st
```json ```json
{ {
"session_id": "SESSION_FROM_OCS_SESSIONS", "ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
"response_detail": "changed_entities", "response_detail": "changed_entities",
"request": { "request": {
"op": "batch", "op": "batch",
@@ -67,11 +126,11 @@ Use `batch` when the steps are already known. OCS supplies each step with the st
Execute responses use `response_detail: "compact"` by default and return only the state needed for the next edit. Use `changed_entities` to receive the current geometry of affected handles in the same response, or `full` when the complete editor state is needed. Execute responses use `response_detail: "compact"` by default and return only the state needed for the next edit. Use `changed_entities` to receive the current geometry of affected handles in the same response, or `full` when the complete editor state is needed.
`ocs_read` query accepts exact handles, type/layer filters, field projection, world-XY bounds, nearest-curve ranking, closed-curve containment, and exact intersections between two planar curves. Nearest points, containment, curve length, area, and intersections are calculated by the geometry kernel: `ocs_read` query accepts exact handles, type/layer filters, field projection, world-XY bounds, nearest-curve ranking, closed-curve containment, and exact intersections between two planar curves. `detail: "full"` adds the complete serialized entity properties. Nearest points, containment, curve length, area, and intersections are calculated by the geometry kernel:
```json ```json
{ {
"session_id": "SESSION_FROM_OCS_SESSIONS", "ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
"op": "query", "op": "query",
"parameters": {"intersections": ["2A", "31"]} "parameters": {"intersections": ["2A", "31"]}
} }
+6 -6
View File
@@ -98,14 +98,14 @@ def main() -> None:
if active.get("start"): if active.get("start"):
client.tool( client.tool(
"ocs_execute", "ocs_execute",
{"session_id": session, "request": {"op": "new", "request_id": "eval-new"}}, {"ocs_session_id": session, "request": {"op": "new", "request_id": "eval-new"}},
) )
base = 1_000_000 + int(time.time()) % 100_000 base = 1_000_000 + int(time.time()) % 100_000
draw = client.tool( draw = client.tool(
"ocs_execute", "ocs_execute",
{ {
"session_id": session, "ocs_session_id": session,
"response_detail": "changed_entities", "response_detail": "changed_entities",
"wait_seconds": 0, "wait_seconds": 0,
"request": { "request": {
@@ -129,19 +129,19 @@ def main() -> None:
crossings = client.tool( crossings = client.tool(
"ocs_read", "ocs_read",
{"session_id": session, "op": "query", "parameters": {"intersections": line_handles}}, {"ocs_session_id": session, "op": "query", "parameters": {"intersections": line_handles}},
) )
assert crossings["count"] == 1 and crossings["intersections"][0]["point"] == [base, 0.0] assert crossings["count"] == 1 and crossings["intersections"][0]["point"] == [base, 0.0]
nearest = client.tool( nearest = client.tool(
"ocs_read", "ocs_read",
{"session_id": session, "op": "query", "parameters": {"near": [base + 20, 0], "handles": handles, "limit": 1}}, {"ocs_session_id": session, "op": "query", "parameters": {"near": [base + 20, 0], "handles": handles, "limit": 1}},
) )
assert nearest["entities"][0]["handle"] == circle_handles[0] assert nearest["entities"][0]["handle"] == circle_handles[0]
measured = client.tool( measured = client.tool(
"ocs_read", "ocs_read",
{"session_id": session, "op": "measure", "parameters": {"handles": circle_handles}}, {"ocs_session_id": session, "op": "measure", "parameters": {"handles": circle_handles}},
) )
assert abs(measured["measurements"][0]["curve"]["area"] - 12.566370614359172) < 1e-9 assert abs(measured["measurements"][0]["curve"]["area"] - 12.566370614359172) < 1e-9
succeeded = True succeeded = True
@@ -151,7 +151,7 @@ def main() -> None:
client.tool( client.tool(
"ocs_execute", "ocs_execute",
{ {
"session_id": session, "ocs_session_id": session,
"request": { "request": {
"op": "batch", "op": "batch",
"request_id": f"eval-clean-{base}", "request_id": f"eval-clean-{base}",
+7 -3
View File
@@ -52,9 +52,13 @@ def legacy(server: Path) -> None:
names = set(definitions) names = set(definitions)
assert names == TOOLS, names assert names == TOOLS, names
execute_request = definitions["ocs_execute"]["inputSchema"]["properties"]["request"] execute_request = definitions["ocs_execute"]["inputSchema"]["properties"]["request"]
assert len(execute_request["oneOf"]) == 15 assert len(execute_request["oneOf"]) == 16
assert execute_request["properties"]["steps"]["maxItems"] == 64 assert execute_request["properties"]["steps"]["maxItems"] == 64
assert execute_request["properties"]["cmd"]["examples"][0] == "LINE 0,0 10,10" assert execute_request["properties"]["cmd"]["examples"][0] == "LINE 0,0 10,10"
assert "set_properties" in execute_request["properties"]["op"]["enum"]
assert "record_schema" in definitions["ocs_read"]["inputSchema"]["properties"]["op"]["enum"]
read_parameters = definitions["ocs_read"]["inputSchema"]["properties"]["parameters"]["properties"]
assert {"collection", "where", "paths"} <= set(read_parameters)
assert execute_request["properties"]["kind"]["enum"] == [ assert execute_request["properties"]["kind"]["enum"] == [
"text", "token", "point", "entity", "structure", "selection", "enter" "text", "token", "point", "entity", "structure", "selection", "enter"
] ]
@@ -110,7 +114,7 @@ def modern(server: Path) -> None:
"params": { "params": {
"name": "ocs_execute", "name": "ocs_execute",
"arguments": { "arguments": {
"session_id": "missing", "ocs_session_id": "missing",
"request": {"op": "run", "request_id": "run-1"}, "request": {"op": "run", "request_id": "run-1"},
}, },
"_meta": meta, "_meta": meta,
@@ -126,7 +130,7 @@ def modern(server: Path) -> None:
"method": "tools/call", "method": "tools/call",
"params": { "params": {
"name": "ocs_execute", "name": "ocs_execute",
"arguments": {"session_id": "missing", "request": {"op": "undo"}}, "arguments": {"ocs_session_id": "missing", "request": {"op": "undo"}},
"_meta": meta, "_meta": meta,
}, },
}) })
+47 -3
View File
@@ -777,7 +777,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: تعذر تحديد نطاق المجسم. .slice-could-not-determine-the-solid-s-extent = SLICE: تعذر تحديد نطاق المجسم.
.slice-cut-at-ax-value-kept-the-half = SLICE: تم القطع عند __ocs_fmt_0__=__ocs_fmt_1__، وأُبقي النصف __ocs_fmt_2__. .slice-cut-at-ax-value-kept-the-half = SLICE: تم القطع عند __ocs_fmt_0__=__ocs_fmt_1__، وأُبقي النصف __ocs_fmt_2__.
.slice-select-exactly-one-solid-created-this-se = SLICE: حدد مجسمًا واحدًا بالضبط أُنشئ في هذه الجلسة. .slice-select-exactly-one-solid-created-this-se = SLICE: حدد مجسمًا واحدًا بالضبط أُنشئ في هذه الجلسة.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: لا يقطع المستوى المجسم على الجانب المحتفظ به. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: لا يقطع المستوى المجسم على الجانب المحتفظ به.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: تم تمرير منحنى عبر __ocs_fmt_0__ نقطة. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: تم تمرير منحنى عبر __ocs_fmt_0__ نقطة.
.splinefit-need-at-least-3-points = SPLINEFIT: يلزم 3 نقاط على الأقل. .splinefit-need-at-least-3-points = SPLINEFIT: يلزم 3 نقاط على الأقل.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: حدد خطًا متعددًا لتمرير منحنى عبره. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: حدد خطًا متعددًا لتمرير منحنى عبره.
@@ -2207,7 +2220,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE محور مستوى القطع [X / Y / Z] (أضف TOP/BOTTOM بالكتابة): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE محور مستوى القطع [X / Y / Z] (أضف TOP/BOTTOM بالكتابة):
.slice-offset-along-x = SLICE الإزاحة على X: .slice-offset-along-x = SLICE الإزاحة على X:
.slice-offset-along-y = SLICE الإزاحة على Y: .slice-offset-along-y = SLICE الإزاحة على Y:
.slice-offset-along-z = SLICE الإزاحة على Z: .slice-offset-along-z = SLICE الإزاحة على Z:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID حدد النقطة الأولى: .solid-specify-first-point = SOLID حدد النقطة الأولى:
.solid-specify-fourth-point = SOLID حدد النقطة الرابعة: .solid-specify-fourth-point = SOLID حدد النقطة الرابعة:
.solid-specify-second-point = SOLID حدد النقطة الثانية: .solid-specify-second-point = SOLID حدد النقطة الثانية:
@@ -3197,7 +3230,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = إهليلجي .elliptical = إهليلجي
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -764,7 +764,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: обхватът на тялото не можа да бъде определен. .slice-could-not-determine-the-solid-s-extent = SLICE: обхватът на тялото не можа да бъде определен.
.slice-cut-at-ax-value-kept-the-half = SLICE: разрязано при __ocs_fmt_0__=__ocs_fmt_1__, запазена е __ocs_fmt_2__ половина. .slice-cut-at-ax-value-kept-the-half = SLICE: разрязано при __ocs_fmt_0__=__ocs_fmt_1__, запазена е __ocs_fmt_2__ половина.
.slice-select-exactly-one-solid-created-this-se = SLICE: избери точно едно тяло, създадено в тази сесия. .slice-select-exactly-one-solid-created-this-se = SLICE: избери точно едно тяло, създадено в тази сесия.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: равнината не пресича тялото откъм запазената страна. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: равнината не пресича тялото откъм запазената страна.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: сплайн прекаран през __ocs_fmt_0__ точки. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: сплайн прекаран през __ocs_fmt_0__ точки.
.splinefit-need-at-least-3-points = SPLINEFIT: нужни са поне 3 точки. .splinefit-need-at-least-3-points = SPLINEFIT: нужни са поне 3 точки.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: избери полилиния, през която да се прекара сплайн. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: избери полилиния, през която да се прекара сплайн.
@@ -2194,7 +2207,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE ос на режещата равнина [X / Y / Z] (добави TOP/BOTTOM чрез въвеждане): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE ос на режещата равнина [X / Y / Z] (добави TOP/BOTTOM чрез въвеждане):
.slice-offset-along-x = SLICE отместване по X: .slice-offset-along-x = SLICE отместване по X:
.slice-offset-along-y = SLICE отместване по Y: .slice-offset-along-y = SLICE отместване по Y:
.slice-offset-along-z = SLICE отместване по Z: .slice-offset-along-z = SLICE отместване по Z:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID Посочи първа точка: .solid-specify-first-point = SOLID Посочи първа точка:
.solid-specify-fourth-point = SOLID Посочи четвърта точка: .solid-specify-fourth-point = SOLID Посочи четвърта точка:
.solid-specify-second-point = SOLID Посочи втора точка: .solid-specify-second-point = SOLID Посочи втора точка:
@@ -3184,7 +3217,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = Елиптичен .elliptical = Елиптичен
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -764,7 +764,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: Nelze určit rozsah tělesa. .slice-could-not-determine-the-solid-s-extent = SLICE: Nelze určit rozsah tělesa.
.slice-cut-at-ax-value-kept-the-half = SLICE: řez na __ocs_fmt_0__=__ocs_fmt_1__, zachování poloviny __ocs_fmt_2__. .slice-cut-at-ax-value-kept-the-half = SLICE: řez na __ocs_fmt_0__=__ocs_fmt_1__, zachování poloviny __ocs_fmt_2__.
.slice-select-exactly-one-solid-created-this-se = SLICE: vyberte přesně jedno těleso vytvořené v této relaci. .slice-select-exactly-one-solid-created-this-se = SLICE: vyberte přesně jedno těleso vytvořené v této relaci.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: rovina neprotíná těleso na držené straně. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: rovina neprotíná těleso na držené straně.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: proložení spline přes body __ocs_fmt_0__. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: proložení spline přes body __ocs_fmt_0__.
.splinefit-need-at-least-3-points =SPLINEFIT: potřebujete alespoň 3 body. .splinefit-need-at-least-3-points =SPLINEFIT: potřebujete alespoň 3 body.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: vyberte křivku pro proložení spline. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: vyberte křivku pro proložení spline.
@@ -2194,7 +2207,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE osa řezné roviny [X / Y / Z] (přidejte TOP/BOTTOM zadáním): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE osa řezné roviny [X / Y / Z] (přidejte TOP/BOTTOM zadáním):
.slice-offset-along-x = Odsazení SLICE podél X: .slice-offset-along-x = Odsazení SLICE podél X:
.slice-offset-along-y = Odsazení SLICE podél Y: .slice-offset-along-y = Odsazení SLICE podél Y:
.slice-offset-along-z = Odsazení SLICE podél Z: .slice-offset-along-z = Odsazení SLICE podél Z:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID Zadejte první bod: .solid-specify-first-point = SOLID Zadejte první bod:
.solid-specify-fourth-point = SOLID Zadejte čtvrtý bod: .solid-specify-fourth-point = SOLID Zadejte čtvrtý bod:
.solid-specify-second-point = SOLID Zadejte druhý bod: .solid-specify-second-point = SOLID Zadejte druhý bod:
@@ -3182,7 +3215,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = Eliptický .elliptical = Eliptický
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -765,7 +765,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: konnte die Ausdehnung des Volumenkörper nicht bestimmen. .slice-could-not-determine-the-solid-s-extent = SLICE: konnte die Ausdehnung des Volumenkörper nicht bestimmen.
.slice-cut-at-ax-value-kept-the-half = SLICE: geschnitten bei __ocs_fmt_0__=__ocs_fmt_1__, die Hälfte von __ocs_fmt_2__ beibehalten. .slice-cut-at-ax-value-kept-the-half = SLICE: geschnitten bei __ocs_fmt_0__=__ocs_fmt_1__, die Hälfte von __ocs_fmt_2__ beibehalten.
.slice-select-exactly-one-solid-created-this-se = SLICE: Wählen Sie genau einen erstellten Volumenkörper. .slice-select-exactly-one-solid-created-this-se = SLICE: Wählen Sie genau einen erstellten Volumenkörper.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: Die Ebene kreuzt den Volumenkörper auf der gehaltenen Seite nicht. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: Die Ebene kreuzt den Volumenkörper auf der gehaltenen Seite nicht.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: Passen Sie einen Spline durch __ocs_fmt_0__ Punkte. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: Passen Sie einen Spline durch __ocs_fmt_0__ Punkte.
.splinefit-need-at-least-3-points = SPLINEFIT: mindestens 3 Punkte werden benötigt. .splinefit-need-at-least-3-points = SPLINEFIT: mindestens 3 Punkte werden benötigt.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: Wählen Sie eine Polylinie, um eine Spline einzupassen. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: Wählen Sie eine Polylinie, um eine Spline einzupassen.
@@ -2195,7 +2208,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE Schnittebenenachse [X / Y / Z] (fügen Sie TOP/BOTTOM durch Eingabe hinzu): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE Schnittebenenachse [X / Y / Z] (fügen Sie TOP/BOTTOM durch Eingabe hinzu):
.slice-offset-along-x = SLICE versetzt entlang X: .slice-offset-along-x = SLICE versetzt entlang X:
.slice-offset-along-y = SLICE längs Y versetzt: .slice-offset-along-y = SLICE längs Y versetzt:
.slice-offset-along-z = SLICE Versetzt entlang Z: .slice-offset-along-z = SLICE Versetzt entlang Z:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID Geben Sie den ersten Punkt an: .solid-specify-first-point = SOLID Geben Sie den ersten Punkt an:
.solid-specify-fourth-point = SOLID Vierter Punkt angeben: .solid-specify-fourth-point = SOLID Vierter Punkt angeben:
.solid-specify-second-point = SOLID Zweiter Punkt angeben: .solid-specify-second-point = SOLID Zweiter Punkt angeben:
@@ -3183,7 +3216,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = Elliptisch .elliptical = Elliptisch
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -766,7 +766,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: δεν ήταν δυνατός ο προσδιορισμός της έκτασης του στερεού. .slice-could-not-determine-the-solid-s-extent = SLICE: δεν ήταν δυνατός ο προσδιορισμός της έκτασης του στερεού.
.slice-cut-at-ax-value-kept-the-half = SLICE: τομή στο __ocs_fmt_0__=__ocs_fmt_1__, διατηρήθηκε το __ocs_fmt_2__ ήμισυ. .slice-cut-at-ax-value-kept-the-half = SLICE: τομή στο __ocs_fmt_0__=__ocs_fmt_1__, διατηρήθηκε το __ocs_fmt_2__ ήμισυ.
.slice-select-exactly-one-solid-created-this-se = SLICE: επιλέξτε ακριβώς ένα στερεό που δημιουργήθηκε σε αυτή τη συνεδρία. .slice-select-exactly-one-solid-created-this-se = SLICE: επιλέξτε ακριβώς ένα στερεό που δημιουργήθηκε σε αυτή τη συνεδρία.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: το επίπεδο δεν τέμνει το στερεό στην πλευρά που διατηρείται. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: το επίπεδο δεν τέμνει το στερεό στην πλευρά που διατηρείται.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: προσαρμογή spline μέσα από __ocs_fmt_0__ σημεία. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: προσαρμογή spline μέσα από __ocs_fmt_0__ σημεία.
.splinefit-need-at-least-3-points = SPLINEFIT: απαιτούνται τουλάχιστον 3 σημεία. .splinefit-need-at-least-3-points = SPLINEFIT: απαιτούνται τουλάχιστον 3 σημεία.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: επιλέξτε μια πολυγραμμή για προσαρμογή spline. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: επιλέξτε μια πολυγραμμή για προσαρμογή spline.
@@ -2194,7 +2207,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE άξονας επιπέδου τομής [X / Y / Z] (προσθέστε TOP/BOTTOM πληκτρολογώντας): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE άξονας επιπέδου τομής [X / Y / Z] (προσθέστε TOP/BOTTOM πληκτρολογώντας):
.slice-offset-along-x = SLICE μετατόπιση κατά X: .slice-offset-along-x = SLICE μετατόπιση κατά X:
.slice-offset-along-y = SLICE μετατόπιση κατά Y: .slice-offset-along-y = SLICE μετατόπιση κατά Y:
.slice-offset-along-z = SLICE μετατόπιση κατά Z: .slice-offset-along-z = SLICE μετατόπιση κατά Z:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID Ορίστε πρώτο σημείο: .solid-specify-first-point = SOLID Ορίστε πρώτο σημείο:
.solid-specify-fourth-point = SOLID Ορίστε τέταρτο σημείο: .solid-specify-fourth-point = SOLID Ορίστε τέταρτο σημείο:
.solid-specify-second-point = SOLID Ορίστε δεύτερο σημείο: .solid-specify-second-point = SOLID Ορίστε δεύτερο σημείο:
@@ -3185,7 +3218,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = Ελλειπτικό .elliptical = Ελλειπτικό
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+44
View File
@@ -767,6 +767,19 @@ common =
.slice-cut-at-ax-value-kept-the-half = SLICE: cut at __ocs_fmt_0__=__ocs_fmt_1__, kept the __ocs_fmt_2__ half. .slice-cut-at-ax-value-kept-the-half = SLICE: cut at __ocs_fmt_0__=__ocs_fmt_1__, kept the __ocs_fmt_2__ half.
.slice-select-exactly-one-solid-created-this-se = SLICE: select exactly one solid created this session. .slice-select-exactly-one-solid-created-this-se = SLICE: select exactly one solid created this session.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: the plane does not cross the solid on the kept side. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: the plane does not cross the solid on the kept side.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: fit a spline through __ocs_fmt_0__ points. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: fit a spline through __ocs_fmt_0__ points.
.splinefit-need-at-least-3-points = SPLINEFIT: need at least 3 points. .splinefit-need-at-least-3-points = SPLINEFIT: need at least 3 points.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: select a polyline to fit a spline through. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: select a polyline to fit a spline through.
@@ -2196,6 +2209,26 @@ draw =
.slice-offset-along-x = SLICE offset along X: .slice-offset-along-x = SLICE offset along X:
.slice-offset-along-y = SLICE offset along Y: .slice-offset-along-y = SLICE offset along Y:
.slice-offset-along-z = SLICE offset along Z: .slice-offset-along-z = SLICE offset along Z:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID Specify first point: .solid-specify-first-point = SOLID Specify first point:
.solid-specify-fourth-point = SOLID Specify fourth point: .solid-specify-fourth-point = SOLID Specify fourth point:
.solid-specify-second-point = SOLID Specify second point: .solid-specify-second-point = SOLID Specify second point:
@@ -3187,6 +3220,17 @@ model =
.cylinder-specify-axis-endpoint = CYLINDER Specify axis endpoint: .cylinder-specify-axis-endpoint = CYLINDER Specify axis endpoint:
.elliptical = Elliptical .elliptical = Elliptical
.select-a-solid-edge = Select a solid edge: .select-a-solid-edge = Select a solid edge:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = Specify fillet radius <__ocs_fmt_0__>: .specify-fillet-radius = Specify fillet radius <__ocs_fmt_0__>:
.specify-chamfer-distance = Specify chamfer distance <__ocs_fmt_0__>: .specify-chamfer-distance = Specify chamfer distance <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID Height = __ocs_fmt_0__, Width = __ocs_fmt_1__, Justification = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID Height = __ocs_fmt_0__, Width = __ocs_fmt_1__, Justification = __ocs_fmt_2__
+47 -3
View File
@@ -765,7 +765,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: no se pudo determinar la extensión del sólido. .slice-could-not-determine-the-solid-s-extent = SLICE: no se pudo determinar la extensión del sólido.
.slice-cut-at-ax-value-kept-the-half = SLICE: corte en __ocs_fmt_0__=__ocs_fmt_1__, se conservó la mitad __ocs_fmt_2__. .slice-cut-at-ax-value-kept-the-half = SLICE: corte en __ocs_fmt_0__=__ocs_fmt_1__, se conservó la mitad __ocs_fmt_2__.
.slice-select-exactly-one-solid-created-this-se = SLICE: seleccione exactamente un sólido creado en esta sesión. .slice-select-exactly-one-solid-created-this-se = SLICE: seleccione exactamente un sólido creado en esta sesión.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: el plano no atraviesa el sólido en el lado conservado. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: el plano no atraviesa el sólido en el lado conservado.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: se ajustó una spline por __ocs_fmt_0__ puntos. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: se ajustó una spline por __ocs_fmt_0__ puntos.
.splinefit-need-at-least-3-points = SPLINEFIT: se necesitan al menos 3 puntos. .splinefit-need-at-least-3-points = SPLINEFIT: se necesitan al menos 3 puntos.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: seleccione una polilínea por la que ajustar una spline. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: seleccione una polilínea por la que ajustar una spline.
@@ -2195,7 +2208,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE eje del plano de corte [X / Y / Z] (escriba TOP/BOTTOM para añadirlo): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE eje del plano de corte [X / Y / Z] (escriba TOP/BOTTOM para añadirlo):
.slice-offset-along-x = SLICE desfase a lo largo de X: .slice-offset-along-x = SLICE desfase a lo largo de X:
.slice-offset-along-y = SLICE desfase a lo largo de Y: .slice-offset-along-y = SLICE desfase a lo largo de Y:
.slice-offset-along-z = SLICE desfase a lo largo de Z: .slice-offset-along-z = SLICE desfase a lo largo de Z:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID Precise el primer punto: .solid-specify-first-point = SOLID Precise el primer punto:
.solid-specify-fourth-point = SOLID Precise el cuarto punto: .solid-specify-fourth-point = SOLID Precise el cuarto punto:
.solid-specify-second-point = SOLID Precise el segundo punto: .solid-specify-second-point = SOLID Precise el segundo punto:
@@ -3185,7 +3218,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = Elíptico .elliptical = Elíptico
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -764,7 +764,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: kiinteän aineen laajuutta ei voitu määrittää. .slice-could-not-determine-the-solid-s-extent = SLICE: kiinteän aineen laajuutta ei voitu määrittää.
.slice-cut-at-ax-value-kept-the-half = SLICE: leikattu kohdasta __ocs_fmt_0__=__ocs_fmt_1__, __ocs_fmt_2__ puolet säilytetty. .slice-cut-at-ax-value-kept-the-half = SLICE: leikattu kohdasta __ocs_fmt_0__=__ocs_fmt_1__, __ocs_fmt_2__ puolet säilytetty.
.slice-select-exactly-one-solid-created-this-se = SLICE: valitse täsmälleen yksi tämän istunnon luotu solid. .slice-select-exactly-one-solid-created-this-se = SLICE: valitse täsmälleen yksi tämän istunnon luotu solid.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: kone ei ylitä kiinteää pintaa säilytetyllä puolella. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: kone ei ylitä kiinteää pintaa säilytetyllä puolella.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: sovita spline __ocs_fmt_0__-pisteiden läpi. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: sovita spline __ocs_fmt_0__-pisteiden läpi.
.splinefit-need-at-least-3-points =SPLINEFIT: tarvitset vähintään 3 pistettä. .splinefit-need-at-least-3-points =SPLINEFIT: tarvitset vähintään 3 pistettä.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: valitse polyline sovittaaksesi splinin läpi. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: valitse polyline sovittaaksesi splinin läpi.
@@ -2194,7 +2207,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE leikkaustason akseli [X / Y / Z] (lisää TOP/BOTTOM kirjoittamalla): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE leikkaustason akseli [X / Y / Z] (lisää TOP/BOTTOM kirjoittamalla):
.slice-offset-along-x = SLICE offset pitkin X: .slice-offset-along-x = SLICE offset pitkin X:
.slice-offset-along-y = SLICE offset pitkin Y: .slice-offset-along-y = SLICE offset pitkin Y:
.slice-offset-along-z = SLICE offset pitkin Z: .slice-offset-along-z = SLICE offset pitkin Z:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID Määritä ensimmäinen piste: .solid-specify-first-point = SOLID Määritä ensimmäinen piste:
.solid-specify-fourth-point = SOLID Määritä neljäs piste: .solid-specify-fourth-point = SOLID Määritä neljäs piste:
.solid-specify-second-point = SOLID Määritä toinen piste: .solid-specify-second-point = SOLID Määritä toinen piste:
@@ -3184,7 +3217,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = Elliptinen .elliptical = Elliptinen
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -765,7 +765,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE : ne pouvait déterminer l'étendue du solide. .slice-could-not-determine-the-solid-s-extent = SLICE : ne pouvait déterminer l'étendue du solide.
.slice-cut-at-ax-value-kept-the-half = SLICE: coupé à __ocs_fmt_0__=__ocs_fmt_1__, maintenu la moitié __ocs_fmt_2__. .slice-cut-at-ax-value-kept-the-half = SLICE: coupé à __ocs_fmt_0__=__ocs_fmt_1__, maintenu la moitié __ocs_fmt_2__.
.slice-select-exactly-one-solid-created-this-se = SLICE : sélectionnez exactement un solide créé cette session. .slice-select-exactly-one-solid-created-this-se = SLICE : sélectionnez exactement un solide créé cette session.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: le plan ne traverse pas le solide du côté retenu. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: le plan ne traverse pas le solide du côté retenu.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: adaptez une élingue à des points __ocs_fmt_0__. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: adaptez une élingue à des points __ocs_fmt_0__.
.splinefit-need-at-least-3-points = SPLINEFIT: nécessite au moins 3 points. .splinefit-need-at-least-3-points = SPLINEFIT: nécessite au moins 3 points.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT : sélectionnez une polyligne pour l'adapter à une spline. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT : sélectionnez une polyligne pour l'adapter à une spline.
@@ -2195,7 +2208,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE axe du plan de coupe [X / Y / Z] (ajouter TOP/BOTTOM par typage): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE axe du plan de coupe [X / Y / Z] (ajouter TOP/BOTTOM par typage):
.slice-offset-along-x = SLICE décalage le long de X: .slice-offset-along-x = SLICE décalage le long de X:
.slice-offset-along-y = SLICE décalage le long de Y: .slice-offset-along-y = SLICE décalage le long de Y:
.slice-offset-along-z = SLICE décalage le long de Z: .slice-offset-along-z = SLICE décalage le long de Z:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID Préciser le premier point: .solid-specify-first-point = SOLID Préciser le premier point:
.solid-specify-fourth-point = SOLID Préciser le quatrième point: .solid-specify-fourth-point = SOLID Préciser le quatrième point:
.solid-specify-second-point = SOLID Préciser le deuxième point: .solid-specify-second-point = SOLID Préciser le deuxième point:
@@ -3185,7 +3218,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = Elliptique .elliptical = Elliptique
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -753,7 +753,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: ठोस की सीमा निर्धारित नहीं कर सकता है। .slice-could-not-determine-the-solid-s-extent = SLICE: ठोस की सीमा निर्धारित नहीं कर सकता है।
.slice-cut-at-ax-value-kept-the-half = SLICE: __ocs_fmt_0__ = __ocs_fmt_1__ में कटौती, __ocs_fmt_2__ आधा रखा। .slice-cut-at-ax-value-kept-the-half = SLICE: __ocs_fmt_0__ = __ocs_fmt_1__ में कटौती, __ocs_fmt_2__ आधा रखा।
.slice-select-exactly-one-solid-created-this-se = SLICE: इस सत्र को ठीक से एक ठोस बनाया गया चुनें। .slice-select-exactly-one-solid-created-this-se = SLICE: इस सत्र को ठीक से एक ठोस बनाया गया चुनें।
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: विमान को रखा पक्ष पर ठोस पार नहीं किया जाता है। .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: विमान को रखा पक्ष पर ठोस पार नहीं किया जाता है।
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ अंक के माध्यम से एक स्पैन फिट। .splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ अंक के माध्यम से एक स्पैन फिट।
.splinefit-need-at-least-3-points = SPLINEFIT: कम से कम 3 अंक की आवश्यकता है। .splinefit-need-at-least-3-points = SPLINEFIT: कम से कम 3 अंक की आवश्यकता है।
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: एक sline के माध्यम से फिट करने के लिए एक पॉलीलाइन का चयन करें। .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: एक sline के माध्यम से फिट करने के लिए एक पॉलीलाइन का चयन करें।
@@ -2183,7 +2196,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE कटिंग-प्लेन अक्ष [X / Y / Z] (Typing द्वारा शीर्ष/BOTTOM को शामिल किया गया): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE कटिंग-प्लेन अक्ष [X / Y / Z] (Typing द्वारा शीर्ष/BOTTOM को शामिल किया गया):
.slice-offset-along-x = SLICE X के साथ ऑफसेट: .slice-offset-along-x = SLICE X के साथ ऑफसेट:
.slice-offset-along-y = SLICE वाई के साथ ऑफसेट: .slice-offset-along-y = SLICE वाई के साथ ऑफसेट:
.slice-offset-along-z = SLICE Z के साथ ऑफसेट: .slice-offset-along-z = SLICE Z के साथ ऑफसेट:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID पहली बिंदु निर्दिष्ट करें: .solid-specify-first-point = SOLID पहली बिंदु निर्दिष्ट करें:
.solid-specify-fourth-point = SOLID चौथे बिंदु को निर्दिष्ट करें: .solid-specify-fourth-point = SOLID चौथे बिंदु को निर्दिष्ट करें:
.solid-specify-second-point = SOLID दूसरे बिंदु को निर्दिष्ट करें: .solid-specify-second-point = SOLID दूसरे बिंदु को निर्दिष्ट करें:
@@ -3173,7 +3206,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = दीर्घवृत्तीय .elliptical = दीर्घवृत्तीय
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -764,7 +764,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: nem tudta meghatározni a szilárdtest kiterjedését. .slice-could-not-determine-the-solid-s-extent = SLICE: nem tudta meghatározni a szilárdtest kiterjedését.
.slice-cut-at-ax-value-kept-the-half = SLICE: vágás a __ocs_fmt_0__=__ocs_fmt_1__ pontnál, a __ocs_fmt_2__ felét megtartotta. .slice-cut-at-ax-value-kept-the-half = SLICE: vágás a __ocs_fmt_0__=__ocs_fmt_1__ pontnál, a __ocs_fmt_2__ felét megtartotta.
.slice-select-exactly-one-solid-created-this-se = SLICE: válasszon ki pontosan egy szilárdtestet, amelyet ebben a munkamenetben hozott létre. .slice-select-exactly-one-solid-created-this-se = SLICE: válasszon ki pontosan egy szilárdtestet, amelyet ebben a munkamenetben hozott létre.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: a sík nem keresztezi a szilárdtestet a megtartott oldalon. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: a sík nem keresztezi a szilárdtestet a megtartott oldalon.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: illesszen be egy spline-t a __ocs_fmt_0__ pontokon. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: illesszen be egy spline-t a __ocs_fmt_0__ pontokon.
.splinefit-need-at-least-3-points =SPLINEFIT: legalább 3 pont szükséges. .splinefit-need-at-least-3-points =SPLINEFIT: legalább 3 pont szükséges.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: válasszon egy vonalláncot a spline átillesztéséhez. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: válasszon egy vonalláncot a spline átillesztéséhez.
@@ -2194,7 +2207,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE vágósík tengelye [X / Y / Z] (adja hozzá a TOP/BOTTOM-t beírással): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE vágósík tengelye [X / Y / Z] (adja hozzá a TOP/BOTTOM-t beírással):
.slice-offset-along-x = SLICE eltolás a X mentén: .slice-offset-along-x = SLICE eltolás a X mentén:
.slice-offset-along-y = SLICE eltolás a Y mentén: .slice-offset-along-y = SLICE eltolás a Y mentén:
.slice-offset-along-z = SLICE eltolás a Z mentén: .slice-offset-along-z = SLICE eltolás a Z mentén:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID Adja meg az első pontot: .solid-specify-first-point = SOLID Adja meg az első pontot:
.solid-specify-fourth-point = SOLID Adja meg a negyedik pontot: .solid-specify-fourth-point = SOLID Adja meg a negyedik pontot:
.solid-specify-second-point = SOLID Második pont megadása: .solid-specify-second-point = SOLID Második pont megadása:
@@ -3184,7 +3217,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = Elliptikus .elliptical = Elliptikus
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -764,7 +764,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: impossibile determinare l'estensione del solido. .slice-could-not-determine-the-solid-s-extent = SLICE: impossibile determinare l'estensione del solido.
.slice-cut-at-ax-value-kept-the-half = SLICE: tagliato a __ocs_fmt_0__=__ocs_fmt_1__, ha mantenuto la metà __ocs_fmt_2__. .slice-cut-at-ax-value-kept-the-half = SLICE: tagliato a __ocs_fmt_0__=__ocs_fmt_1__, ha mantenuto la metà __ocs_fmt_2__.
.slice-select-exactly-one-solid-created-this-se = SLICE: seleziona esattamente un solido creato in questa sessione. .slice-select-exactly-one-solid-created-this-se = SLICE: seleziona esattamente un solido creato in questa sessione.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: il piano non attraversa il solido dal lato mantenuto. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: il piano non attraversa il solido dal lato mantenuto.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: adatta una spline attraverso i punti __ocs_fmt_0__. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: adatta una spline attraverso i punti __ocs_fmt_0__.
.splinefit-need-at-least-3-points =SPLINEFIT: servono almeno 3 punti. .splinefit-need-at-least-3-points =SPLINEFIT: servono almeno 3 punti.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: seleziona una polilinea attraverso la quale inserire una spline. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: seleziona una polilinea attraverso la quale inserire una spline.
@@ -2194,7 +2207,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = Asse del piano di taglio SLICE [X / Y / Z] (aggiungere TOP/BOTTOM digitando): .slice-cutting-plane-axis-x-y-z-add-top-bottom = Asse del piano di taglio SLICE [X / Y / Z] (aggiungere TOP/BOTTOM digitando):
.slice-offset-along-x = Scostamento SLICE lungo X: .slice-offset-along-x = Scostamento SLICE lungo X:
.slice-offset-along-y = Scostamento SLICE lungo Y: .slice-offset-along-y = Scostamento SLICE lungo Y:
.slice-offset-along-z = Scostamento SLICE lungo Z: .slice-offset-along-z = Scostamento SLICE lungo Z:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID Specificare il primo punto: .solid-specify-first-point = SOLID Specificare il primo punto:
.solid-specify-fourth-point = SOLID Specificare il quarto punto: .solid-specify-fourth-point = SOLID Specificare il quarto punto:
.solid-specify-second-point = SOLID Specificare il secondo punto: .solid-specify-second-point = SOLID Specificare il secondo punto:
@@ -3184,7 +3217,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = Ellittico .elliptical = Ellittico
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -755,7 +755,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: ソリッドの範囲を特定できませんでした。 .slice-could-not-determine-the-solid-s-extent = SLICE: ソリッドの範囲を特定できませんでした。
.slice-cut-at-ax-value-kept-the-half = SLICE: __ocs_fmt_0__=__ocs_fmt_1__ で切断し、__ocs_fmt_2__ 側を保持しました。 .slice-cut-at-ax-value-kept-the-half = SLICE: __ocs_fmt_0__=__ocs_fmt_1__ で切断し、__ocs_fmt_2__ 側を保持しました。
.slice-select-exactly-one-solid-created-this-se = SLICE: このセッションで作成したソリッドを 1 つ選択してください。 .slice-select-exactly-one-solid-created-this-se = SLICE: このセッションで作成したソリッドを 1 つ選択してください。
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 保持側で平面がソリッドと交差しません。 .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 保持側で平面がソリッドと交差しません。
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ 点を通るスプラインを作成しました。 .splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ 点を通るスプラインを作成しました。
.splinefit-need-at-least-3-points = SPLINEFIT: 3 点以上必要です。 .splinefit-need-at-least-3-points = SPLINEFIT: 3 点以上必要です。
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: スプラインを通すポリラインを選択してください。 .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: スプラインを通すポリラインを選択してください。
@@ -2185,7 +2198,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE 切断面の軸 [X / Y / Z](TOP/BOTTOM を追加可能): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE 切断面の軸 [X / Y / Z](TOP/BOTTOM を追加可能):
.slice-offset-along-x = SLICE X 方向オフセット: .slice-offset-along-x = SLICE X 方向オフセット:
.slice-offset-along-y = SLICE Y 方向オフセット: .slice-offset-along-y = SLICE Y 方向オフセット:
.slice-offset-along-z = SLICE Z 方向オフセット: .slice-offset-along-z = SLICE Z 方向オフセット:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID 1 点目を指定: .solid-specify-first-point = SOLID 1 点目を指定:
.solid-specify-fourth-point = SOLID 4 点目を指定: .solid-specify-fourth-point = SOLID 4 点目を指定:
.solid-specify-second-point = SOLID 2 点目を指定: .solid-specify-second-point = SOLID 2 点目を指定:
@@ -3173,7 +3206,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = 楕円 .elliptical = 楕円
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -764,7 +764,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: 솔리드의 범위를 확인할 수 없습니다. .slice-could-not-determine-the-solid-s-extent = SLICE: 솔리드의 범위를 확인할 수 없습니다.
.slice-cut-at-ax-value-kept-the-half = SLICE: __ocs_fmt_0__=__ocs_fmt_1__에서 절단하고 __ocs_fmt_2__ 절반을 유지합니다. .slice-cut-at-ax-value-kept-the-half = SLICE: __ocs_fmt_0__=__ocs_fmt_1__에서 절단하고 __ocs_fmt_2__ 절반을 유지합니다.
.slice-select-exactly-one-solid-created-this-se = SLICE: 이번 세션에서 생성된 정확히 하나의 솔리드를 선택합니다. .slice-select-exactly-one-solid-created-this-se = SLICE: 이번 세션에서 생성된 정확히 하나의 솔리드를 선택합니다.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 평면이 유지된 측면의 솔리드를 교차하지 않습니다. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 평면이 유지된 측면의 솔리드를 교차하지 않습니다.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ 점을 통해 스플라인을 맞춥니다. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ 점을 통해 스플라인을 맞춥니다.
.splinefit-need-at-least-3-points =SPLINEFIT: 최소 3개의 포인트가 필요합니다. .splinefit-need-at-least-3-points =SPLINEFIT: 최소 3개의 포인트가 필요합니다.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: 스플라인에 맞게 폴리선을 선택합니다. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: 스플라인에 맞게 폴리선을 선택합니다.
@@ -2194,7 +2207,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE 절단 평면 축 [X / Y / Z] (입력하여 TOP/BOTTOM 추가): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE 절단 평면 축 [X / Y / Z] (입력하여 TOP/BOTTOM 추가):
.slice-offset-along-x = X를 따라 SLICE 오프셋: .slice-offset-along-x = X를 따라 SLICE 오프셋:
.slice-offset-along-y = Y를 따라 SLICE 오프셋: .slice-offset-along-y = Y를 따라 SLICE 오프셋:
.slice-offset-along-z = Z를 따라 SLICE 오프셋: .slice-offset-along-z = Z를 따라 SLICE 오프셋:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID 첫 번째 점 지정: .solid-specify-first-point = SOLID 첫 번째 점 지정:
.solid-specify-fourth-point = SOLID 네 번째 점 지정: .solid-specify-fourth-point = SOLID 네 번째 점 지정:
.solid-specify-second-point = SOLID 두 번째 점 지정: .solid-specify-second-point = SOLID 두 번째 점 지정:
@@ -3184,7 +3217,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = 타원형 .elliptical = 타원형
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+51 -9
View File
@@ -763,7 +763,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: kon de omvang van de vaste stof niet bepalen. .slice-could-not-determine-the-solid-s-extent = SLICE: kon de omvang van de vaste stof niet bepalen.
.slice-cut-at-ax-value-kept-the-half = SLICE: knippen met __ocs_fmt_0__=__ocs_fmt_1__, de helft __ocs_fmt_2__ behouden. .slice-cut-at-ax-value-kept-the-half = SLICE: knippen met __ocs_fmt_0__=__ocs_fmt_1__, de helft __ocs_fmt_2__ behouden.
.slice-select-exactly-one-solid-created-this-se = SLICE: selecteer precies één vaste die deze sessie aangemaakt heeft. .slice-select-exactly-one-solid-created-this-se = SLICE: selecteer precies één vaste die deze sessie aangemaakt heeft.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: het vlak kruist de vaste stof niet aan de kant van de bewaarde zijde. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: het vlak kruist de vaste stof niet aan de kant van de bewaarde zijde.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: pas een spline door __ocs_fmt_0__ punten. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: pas een spline door __ocs_fmt_0__ punten.
.splinefit-need-at-least-3-points = SPLINEFIT: minimaal 3 punten nodig. .splinefit-need-at-least-3-points = SPLINEFIT: minimaal 3 punten nodig.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: selecteer een polylijn om een spline door te passen. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: selecteer een polylijn om een spline door te passen.
@@ -2195,7 +2208,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE snijvlakas [X / Y / Z] (voeg TOP/BOTTOM toe door te typen): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE snijvlakas [X / Y / Z] (voeg TOP/BOTTOM toe door te typen):
.slice-offset-along-x = SLICE offset langs X: .slice-offset-along-x = SLICE offset langs X:
.slice-offset-along-y = SLICE offset langs Y: .slice-offset-along-y = SLICE offset langs Y:
.slice-offset-along-z = SLICE offset langs Z: .slice-offset-along-z = SLICE offset langs Z:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID Geef het eerste punt op: .solid-specify-first-point = SOLID Geef het eerste punt op:
.solid-specify-fourth-point = SOLID Geef het vierde punt op: .solid-specify-fourth-point = SOLID Geef het vierde punt op:
.solid-specify-second-point = SOLID Tweede punt specificeren: .solid-specify-second-point = SOLID Tweede punt specificeren:
@@ -2773,9 +2806,7 @@ annotate =
.markup = Markering .markup = Markering
.multileader = Multiverwijslijn .multileader = Multiverwijslijn
.multileader-draw-order = Tekenvolgorde multiverwijslijn: .multileader-draw-order = Tekenvolgorde multiverwijslijn:
.multiline-text = .multiline-text = Multilijntekst
Multiline
Tekst
.ordinate = Ordinaat .ordinate = Ordinaat
.out = Uit .out = Uit
.qdim-select-geometry-to-dimension-enter-when-d = QDIM Selecteer de te bematen geometrie (Enter om te voltooien): .qdim-select-geometry-to-dimension-enter-when-d = QDIM Selecteer de te bematen geometrie (Enter om te voltooien):
@@ -3142,7 +3173,7 @@ model =
.revolve-entity = Rotatieobject .revolve-entity = Rotatieobject
.revolve-flags = Vlaggen omdraaien .revolve-flags = Vlaggen omdraaien
.spherical = Sferisch .spherical = Sferisch
.subtract = Aftrekken .subtract = Verschil
.surface-construction = Oppervlaktebouw .surface-construction = Oppervlaktebouw
.sweep-path = Vegen / Pad .sweep-path = Vegen / Pad
.sweep-entity = Sweepobject .sweep-entity = Sweepobject
@@ -3154,7 +3185,7 @@ model =
.u-vector-1 = U-vector 1 .u-vector-1 = U-vector 1
.u-vector-2 = U-vector 2 .u-vector-2 = U-vector 2
.uid = UID .uid = UID
.union = Unie .union = Samenvoegen
.v-isolines = V-isolijnen .v-isolines = V-isolijnen
.v-vector-1 = Vector 1 .v-vector-1 = Vector 1
.v-vector-2 = Vector 2 .v-vector-2 = Vector 2
@@ -3185,7 +3216,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = Elliptisch .elliptical = Elliptisch
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
@@ -3880,7 +3922,7 @@ properties =
.monochrome = Monochroom .monochrome = Monochroom
.more-colors = Meer kleuren... .more-colors = Meer kleuren...
.multileader = Multiverwijslijn .multileader = Multiverwijslijn
.multiline = Multiline .multiline = Multilijntekst
.multileader-style = Multiverwijslijnstijl .multileader-style = Multiverwijslijnstijl
.multiple-lines = Meerdere regels .multiple-lines = Meerdere regels
.n-closed = N gesloten .n-closed = N gesloten
+47 -3
View File
@@ -764,7 +764,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: nie można określić zasięgu bryły. .slice-could-not-determine-the-solid-s-extent = SLICE: nie można określić zasięgu bryły.
.slice-cut-at-ax-value-kept-the-half = SLICE: cięcie w __ocs_fmt_0__=__ocs_fmt_1__, zachowanie połowy __ocs_fmt_2__. .slice-cut-at-ax-value-kept-the-half = SLICE: cięcie w __ocs_fmt_0__=__ocs_fmt_1__, zachowanie połowy __ocs_fmt_2__.
.slice-select-exactly-one-solid-created-this-se = SLICE: wybierz dokładnie jedną bryłę utworzoną w tej sesji. .slice-select-exactly-one-solid-created-this-se = SLICE: wybierz dokładnie jedną bryłę utworzoną w tej sesji.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: płaszczyzna nie przecina bryły po zachowanej stronie. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: płaszczyzna nie przecina bryły po zachowanej stronie.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: dopasuj splajn przez punkty __ocs_fmt_0__. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: dopasuj splajn przez punkty __ocs_fmt_0__.
.splinefit-need-at-least-3-points =SPLINEFIT: potrzebne są co najmniej 3 punkty. .splinefit-need-at-least-3-points =SPLINEFIT: potrzebne są co najmniej 3 punkty.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: wybierz polilinię, przez którą ma zostać dopasowany splajn. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: wybierz polilinię, przez którą ma zostać dopasowany splajn.
@@ -2194,7 +2207,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = Oś płaszczyzny cięcia SLICE [X / Y / Z] (dodaj TOP/BOTTOM wpisując): .slice-cutting-plane-axis-x-y-z-add-top-bottom = Oś płaszczyzny cięcia SLICE [X / Y / Z] (dodaj TOP/BOTTOM wpisując):
.slice-offset-along-x = Przesunięcie SLICE wzdłuż X: .slice-offset-along-x = Przesunięcie SLICE wzdłuż X:
.slice-offset-along-y = Przesunięcie SLICE wzdłuż Y: .slice-offset-along-y = Przesunięcie SLICE wzdłuż Y:
.slice-offset-along-z = Przesunięcie SLICE wzdłuż Z: .slice-offset-along-z = Przesunięcie SLICE wzdłuż Z:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID Określ pierwszy punkt: .solid-specify-first-point = SOLID Określ pierwszy punkt:
.solid-specify-fourth-point = SOLID Określ czwarty punkt: .solid-specify-fourth-point = SOLID Określ czwarty punkt:
.solid-specify-second-point = SOLID Określ drugi punkt: .solid-specify-second-point = SOLID Określ drugi punkt:
@@ -3184,7 +3217,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = Eliptyczny .elliptical = Eliptyczny
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -765,7 +765,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: não foi possível determinar a extensão do sólido. .slice-could-not-determine-the-solid-s-extent = SLICE: não foi possível determinar a extensão do sólido.
.slice-cut-at-ax-value-kept-the-half = SLICE: corte em __ocs_fmt_0__=__ocs_fmt_1__, metade __ocs_fmt_2__ mantida. .slice-cut-at-ax-value-kept-the-half = SLICE: corte em __ocs_fmt_0__=__ocs_fmt_1__, metade __ocs_fmt_2__ mantida.
.slice-select-exactly-one-solid-created-this-se = SLICE: selecione exatamente um sólido criado nesta sessão. .slice-select-exactly-one-solid-created-this-se = SLICE: selecione exatamente um sólido criado nesta sessão.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: o plano não atravessa o sólido no lado mantido. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: o plano não atravessa o sólido no lado mantido.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: spline ajustada através de __ocs_fmt_0__ pontos. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: spline ajustada através de __ocs_fmt_0__ pontos.
.splinefit-need-at-least-3-points = SPLINEFIT: são necessários pelo menos 3 pontos. .splinefit-need-at-least-3-points = SPLINEFIT: são necessários pelo menos 3 pontos.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: selecione uma polilinha através da qual ajustar uma spline. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: selecione uma polilinha através da qual ajustar uma spline.
@@ -2195,7 +2208,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE eixo do plano de corte [X / Y / Z] (digite TOP/BOTTOM para adicionar): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE eixo do plano de corte [X / Y / Z] (digite TOP/BOTTOM para adicionar):
.slice-offset-along-x = SLICE deslocamento ao longo de X: .slice-offset-along-x = SLICE deslocamento ao longo de X:
.slice-offset-along-y = SLICE deslocamento ao longo de Y: .slice-offset-along-y = SLICE deslocamento ao longo de Y:
.slice-offset-along-z = SLICE deslocamento ao longo de Z: .slice-offset-along-z = SLICE deslocamento ao longo de Z:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID Especifique o primeiro ponto: .solid-specify-first-point = SOLID Especifique o primeiro ponto:
.solid-specify-fourth-point = SOLID Especifique o quarto ponto: .solid-specify-fourth-point = SOLID Especifique o quarto ponto:
.solid-specify-second-point = SOLID Especifique o segundo ponto: .solid-specify-second-point = SOLID Especifique o segundo ponto:
@@ -3185,7 +3218,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = Elíptico .elliptical = Elíptico
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -768,7 +768,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE: не может определить степень твердого тела. .slice-could-not-determine-the-solid-s-extent = SLICE: не может определить степень твердого тела.
.slice-cut-at-ax-value-kept-the-half = SLICE: вырезать на __ocs_fmt_0__=__ocs_fmt_1__, сохранить __ocs_fmt_2__ половину. .slice-cut-at-ax-value-kept-the-half = SLICE: вырезать на __ocs_fmt_0__=__ocs_fmt_1__, сохранить __ocs_fmt_2__ половину.
.slice-select-exactly-one-solid-created-this-se = SLICE: выберите один твердотельный, созданный на этом сеансе. .slice-select-exactly-one-solid-created-this-se = SLICE: выберите один твердотельный, созданный на этом сеансе.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: плоскость не пересекает твердую часть на удерживаемой стороне. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: плоскость не пересекает твердую часть на удерживаемой стороне.
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: проведите сплин через точки __ocs_fmt_0__. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: проведите сплин через точки __ocs_fmt_0__.
.splinefit-need-at-least-3-points = SPLINEFIT: необходимо не менее 3 баллов. .splinefit-need-at-least-3-points = SPLINEFIT: необходимо не менее 3 баллов.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: выберите полилин, чтобы соответствовать сплину. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: выберите полилин, чтобы соответствовать сплину.
@@ -2198,7 +2211,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE ось режущей плоскости [X/Y/Z] (добавить TOP/BOTTOM путем набора текста): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE ось режущей плоскости [X/Y/Z] (добавить TOP/BOTTOM путем набора текста):
.slice-offset-along-x = SLICE Смещение вдоль X: .slice-offset-along-x = SLICE Смещение вдоль X:
.slice-offset-along-y = SLICE смещение вдоль Y: .slice-offset-along-y = SLICE смещение вдоль Y:
.slice-offset-along-z = SLICE Смещение вдоль Z: .slice-offset-along-z = SLICE Смещение вдоль Z:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID Укажите первую точку: .solid-specify-first-point = SOLID Укажите первую точку:
.solid-specify-fourth-point = SOLID Укажите четвертую точку: .solid-specify-fourth-point = SOLID Укажите четвертую точку:
.solid-specify-second-point = SOLID Укажите вторую точку: .solid-specify-second-point = SOLID Укажите вторую точку:
@@ -3186,7 +3219,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = Эллиптический .elliptical = Эллиптический
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+44
View File
@@ -754,6 +754,19 @@ common =
.slice-cut-at-ax-value-kept-the-half = SLICE: __ocs_fmt_0__=__ocs_fmt_1__ konumunda kesildi, __ocs_fmt_2__ yarısı korundu. .slice-cut-at-ax-value-kept-the-half = SLICE: __ocs_fmt_0__=__ocs_fmt_1__ konumunda kesildi, __ocs_fmt_2__ yarısı korundu.
.slice-select-exactly-one-solid-created-this-se = SLICE: Bu oturumda oluşturulan tam bir katı seçin. .slice-select-exactly-one-solid-created-this-se = SLICE: Bu oturumda oluşturulan tam bir katı seçin.
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: Düzlem, korunan tarafta katıyı kesmiyor. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: Düzlem, korunan tarafta katıyı kesmiyor.
.both = Her ikisi
.slice-select-at-least-one-target = SLICE: En az bir katı veya yüzey seçin.
.slice-invalid-plane = SLICE: Kesme düzlemi geçersiz.
.slice-side-point-on-plane = SLICE: Kesme düzleminden uzakta bir nokta seçin.
.slice-plane-misses-target = SLICE: Kesme düzlemi seçilen her nesneyi kesmiyor; hiçbir nesne değiştirilmedi.
.slice-unsupported-intersection = SLICE: Seçilen geometri desteklenmeyen bir yüzey kesişimi içeriyor; hiçbir nesne değiştirilmedi.
.slice-coincident-plane = SLICE: Kesme düzlemi kaynak geometriyle çakışıyor; hiçbir nesne değiştirilmedi.
.slice-topology-refused = SLICE: Seçilen topoloji güvenle bölünemedi; hiçbir nesne değiştirilmedi.
.slice-result-encode-failed = SLICE: Bir sonuç kodlanamadı; hiçbir nesne değiştirilmedi.
.slice-result-display-failed = SLICE: Bir sonuç eksiksiz görüntülenemedi; hiçbir nesne değiştirilmedi.
.slice-result-commit-failed = SLICE: Bir sonuç işlenemedi; tüm kaynak nesneler korundu.
.slice-retained-update-failed = SLICE: Korunan sonuç güncellenemedi.
.slice-created-results = SLICE: __ocs_fmt_0__ sonuç nesnesi oluşturuldu.
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ noktadan geçen bir spline uyduruldu. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ noktadan geçen bir spline uyduruldu.
.splinefit-need-at-least-3-points = SPLINEFIT: En az 3 nokta gerekir. .splinefit-need-at-least-3-points = SPLINEFIT: En az 3 nokta gerekir.
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: İçinden spline geçirilecek bir çoklu çizgi seçin. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: İçinden spline geçirilecek bir çoklu çizgi seçin.
@@ -2177,6 +2190,26 @@ draw =
.slice-offset-along-x = SLICE X boyunca ofset: .slice-offset-along-x = SLICE X boyunca ofset:
.slice-offset-along-y = SLICE Y boyunca ofset: .slice-offset-along-y = SLICE Y boyunca ofset:
.slice-offset-along-z = SLICE Z boyunca ofset: .slice-offset-along-z = SLICE Z boyunca ofset:
.slice-select-targets = SLICE Dilimlenecek katıları veya yüzeyleri seçin, ardından Enter'a basın:
.slice-first-point-or-method = SLICE Kesme düzlemindeki ilk noktayı belirtin veya [düzlemsel Nesne/Yüzey/Zekseni/Görünüm/XY/YZ/ZX/3nokta] <3nokta>:
.slice-first-plane-point = SLICE Kesme düzlemindeki ilk noktayı belirtin:
.slice-second-plane-point = SLICE Kesme düzlemindeki ikinci noktayı belirtin:
.slice-third-plane-point = SLICE Kesme düzlemindeki üçüncü noktayı belirtin:
.slice-point-on-plane = SLICE Kesme düzleminde bir nokta belirtin:
.slice-point-on-normal-axis = SLICE Düzlemin normal ekseninde bir nokta belirtin:
.slice-point-on-view-plane = SLICE Geçerli görünüm düzleminde bir nokta belirtin:
.slice-point-on-ucs-plane = SLICE Geçerli UCS düzleminde bir nokta belirtin:
.slice-select-planar-object = SLICE Düzlemsel bir eğri nesnesi seçin:
.slice-select-planar-surface = SLICE Düzlemsel bir yüzey seçin:
.slice-select-cutting-surface = SLICE Bir kesme yüzeyi seçin:
.slice-select-side-or-both = SLICE İstenen tarafta bir nokta belirtin veya [Her ikisi] <Her ikisi>:
.slice-select-object-or-both = SLICE Korunacak dilimlenmiş nesneyi seçin veya [Her ikisi] <Her ikisi>:
.slice-surface-not-planar = SLICE Seçilen yüzey düzlemsel değil. Düzlemsel bir yüzey seçin:
.slice-surface-not-supported = SLICE Seçilen yüzey desteklenen tek bir kesme yüzeyi tanımlamıyor. Başka bir yüzey seçin:
.slice-object-not-planar = SLICE Seçilen nesne bir düzlem tanımlamıyor. Düzlemsel bir eğri nesnesi seçin:
.slice-planar-object = düzlemsel Nesne
.slice-zaxis = Zekseni
.slice-three-points = 3nokta
.solid-specify-first-point = SOLID İlk noktayı belirtin: .solid-specify-first-point = SOLID İlk noktayı belirtin:
.solid-specify-fourth-point = SOLID Dördüncü noktayı belirtin: .solid-specify-fourth-point = SOLID Dördüncü noktayı belirtin:
.solid-specify-second-point = SOLID İkinci noktayı belirtin: .solid-specify-second-point = SOLID İkinci noktayı belirtin:
@@ -3146,6 +3179,17 @@ model =
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = Eliptik .elliptical = Eliptik
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Bir kenar seçin veya [Zincir/Döngü/Yarıçap]:
.filletedge-select-edge-chain-or-edge-radius = Bir kenar zinciri seçin veya [Kenar/Yarıçap]:
.filletedge-select-loop-edge-or-edge-chain-radius = Döngünün kenarını seçin veya [Kenar/Zincir/Yarıçap]:
.filletedge-enter-option-accept-next = Bir seçenek girin [Kabul/Sonraki] <Kabul>:
.filletedge-accept-or-radius = Fillet'i kabul etmek için Enter'a basın veya [Yarıçap]:
.filletedge-enter-radius-or-expression = Fillet yarıçapını girin veya [İfade] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Yarıçapı belirtin veya [İfade] <__ocs_fmt_0__>:
.filletedge-enter-expression = İfadeyi girin:
.filletedge-stale-edge = Seçilen kenar artık katıya ait değil.
.filletedge-history-create-failed = Katı geçmişi oluşturulamadı.
.filletedge-history-append-failed = Fillet geçmişi kaydedilemedi.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -752,7 +752,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE:无法确定固体的大小。 .slice-could-not-determine-the-solid-s-extent = SLICE:无法确定固体的大小。
.slice-cut-at-ax-value-kept-the-half = SLICE: 切到__ocs_fmt_0__=__ocs_fmt_1__, 保留__ocs_fmt_2__的一半。 .slice-cut-at-ax-value-kept-the-half = SLICE: 切到__ocs_fmt_0__=__ocs_fmt_1__, 保留__ocs_fmt_2__的一半。
.slice-select-exactly-one-solid-created-this-se = SLICE:选择一个创建此会话的正体 。 .slice-select-exactly-one-solid-created-this-se = SLICE:选择一个创建此会话的正体 。
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 飞机不会越过所保存的一侧的固体。 .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 飞机不会越过所保存的一侧的固体。
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: 匹配一条通过__ocs_fmt_0__点的相片。 .splinefit-fit-a-spline-through-n-points = SPLINEFIT: 匹配一条通过__ocs_fmt_0__点的相片。
.splinefit-need-at-least-3-points = SPLINEFIT:至少需要3个点。 .splinefit-need-at-least-3-points = SPLINEFIT:至少需要3个点。
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT:选择一条多线以适应通过的分线。 .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT:选择一条多线以适应通过的分线。
@@ -2182,7 +2195,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE 剖切平面轴 [X / Y / Z](输入 TOP/BOTTOM 可追加): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE 剖切平面轴 [X / Y / Z](输入 TOP/BOTTOM 可追加):
.slice-offset-along-x = SLICE 页:1 .slice-offset-along-x = SLICE 页:1
.slice-offset-along-y = SLICE 与 Y 相冲: .slice-offset-along-y = SLICE 与 Y 相冲:
.slice-offset-along-z = SLICE 与 Z 相冲: .slice-offset-along-z = SLICE 与 Z 相冲:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID 指定第一点: .solid-specify-first-point = SOLID 指定第一点:
.solid-specify-fourth-point = SOLID 指定第四点: .solid-specify-fourth-point = SOLID 指定第四点:
.solid-specify-second-point = SOLID 指定第二点: .solid-specify-second-point = SOLID 指定第二点:
@@ -3168,7 +3201,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = 椭圆 .elliptical = 椭圆
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+47 -3
View File
@@ -764,7 +764,20 @@ common =
.slice-could-not-determine-the-solid-s-extent = SLICE:無法確定固體的大小。 .slice-could-not-determine-the-solid-s-extent = SLICE:無法確定固體的大小。
.slice-cut-at-ax-value-kept-the-half = SLICE: 切到__ocs_fmt_0__=__ocs_fmt_1__, 保留__ocs_fmt_2__的一半。 .slice-cut-at-ax-value-kept-the-half = SLICE: 切到__ocs_fmt_0__=__ocs_fmt_1__, 保留__ocs_fmt_2__的一半。
.slice-select-exactly-one-solid-created-this-se = SLICE:選擇一個建立此會話的正體 。 .slice-select-exactly-one-solid-created-this-se = SLICE:選擇一個建立此會話的正體 。
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 平面不會越過所儲存的一側的固體。 .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 平面不會越過所儲存的一側的固體。
.both = Both
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
.slice-invalid-plane = SLICE: the cutting plane is invalid.
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
.slice-retained-update-failed = SLICE: a retained result could not be updated.
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: 匹配一條通過__ocs_fmt_0__點的相片。 .splinefit-fit-a-spline-through-n-points = SPLINEFIT: 匹配一條通過__ocs_fmt_0__點的相片。
.splinefit-need-at-least-3-points = SPLINEFIT:至少需要3個點。 .splinefit-need-at-least-3-points = SPLINEFIT:至少需要3個點。
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT:選擇一條多線以適應通過的分線。 .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT:選擇一條多線以適應通過的分線。
@@ -2194,7 +2207,27 @@ draw =
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE 剖切平面軸 [X / Y / Z](輸入 TOP/BOTTOM 可追加): .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE 剖切平面軸 [X / Y / Z](輸入 TOP/BOTTOM 可追加):
.slice-offset-along-x = SLICE 頁:1 .slice-offset-along-x = SLICE 頁:1
.slice-offset-along-y = SLICE 與 Y 相衝: .slice-offset-along-y = SLICE 與 Y 相衝:
.slice-offset-along-z = SLICE 與 Z 相衝: .slice-offset-along-z = SLICE 與 Z 相衝:
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
.slice-first-plane-point = SLICE Specify first point on slicing plane:
.slice-second-plane-point = SLICE Specify second point on slicing plane:
.slice-third-plane-point = SLICE Specify third point on slicing plane:
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
.slice-select-planar-object = SLICE Select a planar curve object:
.slice-select-planar-surface = SLICE Select a planar surface:
.slice-select-cutting-surface = SLICE Select a cutting surface:
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
.slice-planar-object = planar Object
.slice-zaxis = Zaxis
.slice-three-points = 3points
.solid-specify-first-point = SOLID 指定第一點: .solid-specify-first-point = SOLID 指定第一點:
.solid-specify-fourth-point = SOLID 指定第四點: .solid-specify-fourth-point = SOLID 指定第四點:
.solid-specify-second-point = SOLID 指定第二點: .solid-specify-second-point = SOLID 指定第二點:
@@ -3180,7 +3213,18 @@ model =
.cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }:
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
.elliptical = 橢圓 .elliptical = 橢圓
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
.filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]:
.filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]:
.filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]:
.filletedge-enter-option-accept-next = Enter an option [Accept/Next] <Accept>:
.filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]:
.filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>:
.filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>:
.filletedge-enter-expression = Enter expression:
.filletedge-stale-edge = A selected edge no longer belongs to the solid.
.filletedge-history-create-failed = The solid history could not be created.
.filletedge-history-append-failed = The fillet history could not be recorded.
.specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>:
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
+4 -2
View File
@@ -103,9 +103,11 @@ if [ "$DEVELOPER_ID" = "-" ]; then
# CI-parity ad-hoc signature; cannot be notarized. # CI-parity ad-hoc signature; cannot be notarized.
codesign --force --deep --sign - --timestamp=none "$APP" codesign --force --deep --sign - --timestamp=none "$APP"
else else
# Inside-out: the appex first (sandbox entitlement is REQUIRED for a # Sign nested code before the outer bundle. The sandbox entitlement is
# QuickLook extension to run), then the outer bundle. Hardened runtime # required for the QuickLook extension. Use hardened runtime
# and a secure timestamp on every layer for notarization. # and a secure timestamp on every layer for notarization.
codesign --force --timestamp --options runtime \
-s "$DEVELOPER_ID" "$APP/Contents/MacOS/OpenCADStudio-App"
codesign --force --timestamp --options runtime \ codesign --force --timestamp --options runtime \
--entitlements crates/dwg-thumbnailer/macos/entitlements.plist \ --entitlements crates/dwg-thumbnailer/macos/entitlements.plist \
-s "$DEVELOPER_ID" "$APP/Contents/PlugIns/DWGThumbnail.appex" -s "$DEVELOPER_ID" "$APP/Contents/PlugIns/DWGThumbnail.appex"
+1 -1
View File
@@ -7,7 +7,7 @@
{ {
"repo": "mf4633/opencad-storm-sewer-plugin", "repo": "mf4633/opencad-storm-sewer-plugin",
"name": "Storm Sewer", "name": "Storm Sewer",
"description": "Gravity storm-drain network design and analysis (Rational + Manning + HGL, LandXML import)." "description": "Gravity storm-drain network design and analysis (Rational + Manning + HGL, LandXML import). Free to draw and analyze; Pro ($29/yr) adds HTML reports, pipe sizing, multi-RP."
}, },
{ {
"repo": "mf4633/opencad-hydrocomplete-plugin", "repo": "mf4633/opencad-hydrocomplete-plugin",
+272 -78
View File
@@ -18,7 +18,7 @@
use std::io::{BufRead, Write}; use std::io::{BufRead, Write};
use std::path::PathBuf; use std::path::PathBuf;
use serde_json::{json, Value}; use serde_json::{Value, json};
use super::OpenCADStudio; use super::OpenCADStudio;
@@ -138,6 +138,14 @@ fn v3(v: acadrust::types::Vector3) -> Value {
json!([v.x, v.y, v.z]) json!([v.x, v.y, v.z])
} }
fn entity_type_matches(entity: &acadrust::EntityType, requested: &str) -> bool {
if crate::entities::names::ui_name(entity).eq_ignore_ascii_case(requested) {
return true;
}
let record_name = crate::entities::names::dxf_name(entity);
record_name != "ENTITY" && record_name.eq_ignore_ascii_case(requested)
}
/// One entity as JSON. Summary mode carries identity only, geometry adds the /// One entity as JSON. Summary mode carries identity only, geometry adds the
/// entity's defining values, and full also includes its world bounds. /// entity's defining values, and full also includes its world bounds.
fn entity_json(e: &acadrust::EntityType, detail: &str) -> Value { fn entity_json(e: &acadrust::EntityType, detail: &str) -> Value {
@@ -194,15 +202,30 @@ fn entity_json(e: &acadrust::EntityType, detail: &str) -> Value {
} }
E::Insert(ins) => { E::Insert(ins) => {
map.insert("block".into(), json!(ins.block_name)); map.insert("block".into(), json!(ins.block_name));
map.insert("position".into(), v3(ins.insert_point));
let attributes: serde_json::Map<String, Value> = ins
.attributes
.iter()
.map(|attribute| (attribute.tag.clone(), json!(attribute.value)))
.collect();
map.insert("attributes".into(), Value::Object(attributes));
} }
_ => {} _ => {}
} }
if detail == "full" { if detail == "full" {
let bounds = e.as_entity().bounding_box(); let bounds = e.as_entity().bounding_box();
map.insert("bounds".into(), json!({ map.insert(
"min":[bounds.min.x,bounds.min.y,bounds.min.z], "bounds".into(),
"max":[bounds.max.x,bounds.max.y,bounds.max.z] json!({
})); "min":[bounds.min.x,bounds.min.y,bounds.min.z],
"max":[bounds.max.x,bounds.max.y,bounds.max.z]
}),
);
if let Ok(Value::Object(wrapper)) = serde_json::to_value(e) {
if let Some((_, properties)) = wrapper.into_iter().next() {
map.insert("properties".into(), properties);
}
}
} }
obj obj
} }
@@ -218,16 +241,24 @@ fn request_point(req: &Value, key: &str) -> Option<[f64; 2]> {
} }
fn request_handle(value: &Value) -> Option<acadrust::Handle> { fn request_handle(value: &Value) -> Option<acadrust::Handle> {
value.as_str() value
.and_then(|value| u64::from_str_radix(value.trim_start_matches("0x"), 16).ok()) .as_str()
.and_then(|value| {
let value = value
.strip_prefix("0x")
.or_else(|| value.strip_prefix("0X"))
.unwrap_or(value);
u64::from_str_radix(value, 16).ok()
})
.map(acadrust::Handle::new) .map(acadrust::Handle::new)
} }
fn projected_fields(mut entity: Value, fields: Option<&Vec<Value>>) -> Value { fn projected_fields(mut entity: Value, fields: Option<&Vec<Value>>) -> Value {
let Some(fields) = fields else { return entity }; let Some(fields) = fields else { return entity };
let Some(source) = entity.as_object_mut() else { return entity }; let Some(source) = entity.as_object_mut() else {
let keep: std::collections::HashSet<&str> = return entity;
fields.iter().filter_map(Value::as_str).collect(); };
let keep: std::collections::HashSet<&str> = fields.iter().filter_map(Value::as_str).collect();
source.retain(|key, _| key == "handle" || keep.contains(key.as_str())); source.retain(|key, _| key == "handle" || keep.contains(key.as_str()));
entity entity
} }
@@ -244,7 +275,9 @@ impl OpenCADStudio {
return err(error); return err(error);
} }
if matches!(response["status"].as_str(), Some("accepted" | "running")) { if matches!(response["status"].as_str(), Some("accepted" | "running")) {
return self.control_request(json!({"op":"operation","request_id":id})).0; return self
.control_request(json!({"op":"operation","request_id":id}))
.0;
} }
return response; return response;
} }
@@ -327,7 +360,9 @@ impl OpenCADStudio {
let i = self.active_tab; let i = self.active_tab;
let before = self.tabs[i].scene.document.entities().count(); let before = self.tabs[i].scene.document.entities().count();
let error_revision = self.command_line.error_revision; let error_revision = self.command_line.error_revision;
if let Err(error) = self.run_headless(&cmd) { return err(error); } if let Err(error) = self.run_headless(&cmd) {
return err(error);
}
if self.command_line.error_revision != error_revision { if self.command_line.error_revision != error_revision {
return err(self.command_line.last_error.clone().unwrap_or_default()); return err(self.command_line.last_error.clone().unwrap_or_default());
} }
@@ -342,13 +377,15 @@ impl OpenCADStudio {
} }
"entities" => self.entity_summary(), "entities" => self.entity_summary(),
"query" => self.entity_query(&req), "query" => self.entity_query(&req),
"records" => self.record_query(&req),
"record_schema" => self.record_schema(&req),
"capabilities" => self.record_capabilities(),
"layers" => { "layers" => {
let i = self.active_tab; let i = self.active_tab;
let offset = req["offset"].as_u64().unwrap_or(0) as usize; let offset = req["offset"].as_u64().unwrap_or(0) as usize;
let limit = req["limit"].as_u64().unwrap_or(1000).min(10_000) as usize; let limit = req["limit"].as_u64().unwrap_or(1000).min(10_000) as usize;
let count = self.tabs[i].scene.document.layers.iter().count(); let count = self.tabs[i].scene.document.layers.iter().count();
let layers: Vec<Value> = self let layers: Vec<Value> = self.tabs[i]
.tabs[i]
.scene .scene
.document .document
.layers .layers
@@ -408,8 +445,14 @@ impl OpenCADStudio {
// By explicit handles (hex, as returned by `query`). // By explicit handles (hex, as returned by `query`).
if let Some(arr) = req["handles"].as_array() { if let Some(arr) = req["handles"].as_array() {
for h in arr.iter().filter_map(|h| h.as_str()) { for h in arr.iter().filter_map(|h| h.as_str()) {
if let Ok(v) = u64::from_str_radix(h.trim_start_matches("0x"), 16) { let h = h
self.tabs[i].scene.select_entity(acadrust::Handle::new(v), false); .strip_prefix("0x")
.or_else(|| h.strip_prefix("0X"))
.unwrap_or(h);
if let Ok(v) = u64::from_str_radix(h, 16) {
self.tabs[i]
.scene
.select_entity(acadrust::Handle::new(v), false);
} }
} }
} }
@@ -421,11 +464,7 @@ impl OpenCADStudio {
.scene .scene
.document .document
.entities() .entities()
.filter(|e| { .filter(|e| type_filter.is_none_or(|t| entity_type_matches(e, t)))
type_filter.is_none_or(|t| {
crate::entities::names::ui_name(e).eq_ignore_ascii_case(t)
})
})
.filter(|e| layer_filter.is_none_or(|l| e.common().layer == l)) .filter(|e| layer_filter.is_none_or(|l| e.common().layer == l))
.map(|e| e.common().handle) .map(|e| e.common().handle)
.collect(); .collect();
@@ -446,8 +485,7 @@ impl OpenCADStudio {
return err("save: no \"path\" and the document has none"); return err("save: no \"path\" and the document has none");
}; };
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
let result = let result = self.save_tab_synchronously_protected(i, path.clone(), true);
self.save_tab_synchronously_protected(i, path.clone(), true);
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
let result = crate::io::save(&self.tabs[i].scene.document, &path) let result = crate::io::save(&self.tabs[i].scene.document, &path)
.map_err(crate::io::SaveFailure::other); .map_err(crate::io::SaveFailure::other);
@@ -472,19 +510,30 @@ impl OpenCADStudio {
self.drive_headless_task(task) self.drive_headless_task(task)
} }
pub(super) fn drive_headless_task(&mut self, task: iced::Task<super::Message>) -> Result<(), String> { pub(super) fn drive_headless_task(
&mut self,
task: iced::Task<super::Message>,
) -> Result<(), String> {
use iced::futures::StreamExt; use iced::futures::StreamExt;
let mut streams = Vec::new(); let mut streams = Vec::new();
if let Some(stream) = iced_runtime::task::into_stream(task) { streams.push(stream); } if let Some(stream) = iced_runtime::task::into_stream(task) {
streams.push(stream);
}
while let Some(stream) = streams.last_mut() { while let Some(stream) = streams.last_mut() {
match iced::futures::executor::block_on(stream.next()) { match iced::futures::executor::block_on(stream.next()) {
Some(iced_runtime::Action::Output(message)) => { Some(iced_runtime::Action::Output(message)) => {
let next = self.update(message); let next = self.update(message);
if let Some(stream) = iced_runtime::task::into_stream(next) { streams.push(stream); } if let Some(stream) = iced_runtime::task::into_stream(next) {
streams.push(stream);
}
} }
Some(iced_runtime::Action::Widget(_)) | Some(iced_runtime::Action::Tick) | Some(iced_runtime::Action::Reload) => {}, Some(iced_runtime::Action::Widget(_))
| Some(iced_runtime::Action::Tick)
| Some(iced_runtime::Action::Reload) => {}
Some(action) => return Err(format!("GUI runtime required for {action:?}")), Some(action) => return Err(format!("GUI runtime required for {action:?}")),
None => { streams.pop(); } None => {
streams.pop();
}
} }
} }
self.finish_all_pending_history(); self.finish_all_pending_history();
@@ -540,20 +589,36 @@ impl OpenCADStudio {
let layer_filter = req["layer"].as_str(); let layer_filter = req["layer"].as_str();
let handles: Option<std::collections::HashSet<u64>> = req["handles"] let handles: Option<std::collections::HashSet<u64>> = req["handles"]
.as_array() .as_array()
.map(|values| values.iter().filter_map(request_handle).map(|h| h.value()).collect()) .map(|values| {
.or_else(|| request_handle(&req["handle"]) values
.map(|handle| std::iter::once(handle.value()).collect())); .iter()
.filter_map(request_handle)
.map(|h| h.value())
.collect()
})
.or_else(|| {
request_handle(&req["handle"])
.map(|handle| std::iter::once(handle.value()).collect())
});
let near = request_point(req, "near"); let near = request_point(req, "near");
let contains = request_point(req, "contains_point"); let contains = request_point(req, "contains_point");
let bounds = req["bounds"].as_array().and_then(|values| { let bounds = req["bounds"].as_array().and_then(|values| {
(values.len() == 4).then(|| Some([ (values.len() == 4)
values[0].as_f64()?, values[1].as_f64()?, .then(|| {
values[2].as_f64()?, values[3].as_f64()?, Some([
])).flatten() values[0].as_f64()?,
values[1].as_f64()?,
values[2].as_f64()?,
values[3].as_f64()?,
])
})
.flatten()
}); });
if req.get("handles").is_some() && handles.as_ref().is_some_and(|parsed| { if req.get("handles").is_some()
parsed.len() != req["handles"].as_array().map_or(0, Vec::len) && handles
}) { .as_ref()
.is_some_and(|parsed| parsed.len() != req["handles"].as_array().map_or(0, Vec::len))
{
return err("query handles contains an invalid hexadecimal handle"); return err("query handles contains an invalid hexadecimal handle");
} }
if req.get("handle").is_some() && request_handle(&req["handle"]).is_none() { if req.get("handle").is_some() && request_handle(&req["handle"]).is_none() {
@@ -565,10 +630,13 @@ impl OpenCADStudio {
if req.get("contains_point").is_some() && contains.is_none() { if req.get("contains_point").is_some() && contains.is_none() {
return err("query contains_point expects two or three finite coordinates"); return err("query contains_point expects two or three finite coordinates");
} }
if req.get("bounds").is_some() && bounds.is_none_or(|bounds| { if req.get("bounds").is_some()
!bounds.iter().all(|value| value.is_finite()) && bounds.is_none_or(|bounds| {
|| bounds[0] > bounds[2] || bounds[1] > bounds[3] !bounds.iter().all(|value| value.is_finite())
}) { || bounds[0] > bounds[2]
|| bounds[1] > bounds[3]
})
{
return err("query bounds expects finite [min_x,min_y,max_x,max_y]"); return err("query bounds expects finite [min_x,min_y,max_x,max_y]");
} }
let detail = req["detail"].as_str().unwrap_or("geometry"); let detail = req["detail"].as_str().unwrap_or("geometry");
@@ -580,39 +648,47 @@ impl OpenCADStudio {
let mut matched = Vec::new(); let mut matched = Vec::new();
for e in tab.scene.document.entities() { for e in tab.scene.document.entities() {
if handles.as_ref().is_some_and(|handles| { if handles
!handles.contains(&e.common().handle.value()) .as_ref()
}) { .is_some_and(|handles| !handles.contains(&e.common().handle.value()))
{
continue; continue;
} }
if type_filter.is_some_and(|value| { if type_filter.is_some_and(|value| !entity_type_matches(e, value))
!crate::entities::names::ui_name(e).eq_ignore_ascii_case(value) || layer_filter.is_some_and(|value| e.common().layer != value)
}) || layer_filter.is_some_and(|value| e.common().layer != value) { {
continue; continue;
} }
if let Some(bounds) = bounds { if let Some(bounds) = bounds {
let entity_bounds = e.as_entity().bounding_box(); let entity_bounds = e.as_entity().bounding_box();
if entity_bounds.max.x < bounds[0] || entity_bounds.max.y < bounds[1] if entity_bounds.max.x < bounds[0]
|| entity_bounds.min.x > bounds[2] || entity_bounds.min.y > bounds[3] || entity_bounds.max.y < bounds[1]
|| entity_bounds.min.x > bounds[2]
|| entity_bounds.min.y > bounds[3]
{ {
continue; continue;
} }
} }
let curve = (near.is_some() || contains.is_some()) let curve = (near.is_some() || contains.is_some())
.then(|| crate::entities::curve::entity_curve_xy(e)).flatten(); .then(|| crate::entities::curve::entity_curve_xy(e))
.flatten();
if let Some(point) = contains { if let Some(point) = contains {
let Some(curve) = curve.as_ref().filter(|curve| curve.is_closed()) else { let Some(curve) = curve.as_ref().filter(|curve| curve.is_closed()) else {
continue; continue;
}; };
if !cadkernel::geom2d::contains( if !cadkernel::geom2d::contains(
std::slice::from_ref(curve), point, std::slice::from_ref(curve),
point,
cadkernel::geom2d::Tolerance::default(), cadkernel::geom2d::Tolerance::default(),
) { ) {
continue; continue;
} }
} }
let nearest = near.and_then(|point| curve.as_ref() let nearest = near.and_then(|point| {
.map(|curve| cadkernel::geom2d::closest_point(curve, point))); curve
.as_ref()
.map(|curve| cadkernel::geom2d::closest_point(curve, point))
});
if near.is_some() && nearest.is_none() { if near.is_some() && nearest.is_none() {
continue; continue;
} }
@@ -620,20 +696,29 @@ impl OpenCADStudio {
if let Some(nearest) = nearest { if let Some(nearest) = nearest {
let object = entity.as_object_mut().expect("entity JSON object"); let object = entity.as_object_mut().expect("entity JSON object");
object.insert("distance".into(), json!(nearest.distance)); object.insert("distance".into(), json!(nearest.distance));
object.insert("closest_point".into(), object.insert(
json!([nearest.point[0], nearest.point[1]])); "closest_point".into(),
json!([nearest.point[0], nearest.point[1]]),
);
object.insert("parameter".into(), json!(nearest.t)); object.insert("parameter".into(), json!(nearest.t));
} }
matched.push((nearest.map(|nearest| nearest.distance), entity)); matched.push((nearest.map(|nearest| nearest.distance), entity));
} }
if near.is_some() { if near.is_some() {
matched.sort_by(|left, right| left.0.partial_cmp(&right.0) matched.sort_by(|left, right| {
.unwrap_or(std::cmp::Ordering::Equal)); left.0
.partial_cmp(&right.0)
.unwrap_or(std::cmp::Ordering::Equal)
});
} }
let count = matched.len(); let count = matched.len();
let fields = req["fields"].as_array(); let fields = req["fields"].as_array();
let entities: Vec<Value> = matched.into_iter().skip(offset).take(limit) let entities: Vec<Value> = matched
.map(|(_, entity)| projected_fields(entity, fields)).collect(); .into_iter()
.skip(offset)
.take(limit)
.map(|(_, entity)| projected_fields(entity, fields))
.collect();
json!({ json!({
"ok": true, "ok": true,
"document_id":tab.id, "document_id":tab.id,
@@ -668,7 +753,10 @@ mod tests {
fn layout_notice_skips_grid_camera_and_scene_builds() { fn layout_notice_skips_grid_camera_and_scene_builds() {
let mut app = OpenCADStudio::new_for_test(); let mut app = OpenCADStudio::new_for_test();
assert_eq!(app.automation_op(r#"{"op":"new"}"#)["ok"], true); assert_eq!(app.automation_op(r#"{"op":"new"}"#)["ok"], true);
assert_eq!(app.automation_op(r#"{"op":"run","cmd":"LINE 0,0 10,10"}"#)["ok"], true); assert_eq!(
app.automation_op(r#"{"op":"run","cmd":"LINE 0,0 10,10"}"#)["ok"],
true
);
let i = app.active_tab; let i = app.active_tab;
let scene = &mut app.tabs[i].scene; let scene = &mut app.tabs[i].scene;
scene.document.add_layout("Review").unwrap(); scene.document.add_layout("Review").unwrap();
@@ -749,6 +837,44 @@ mod tests {
assert_eq!(app.automation_op(r#"{"op":"run"}"#)["ok"], false); assert_eq!(app.automation_op(r#"{"op":"run"}"#)["ok"], false);
} }
#[test]
fn block_reference_query_exposes_instance_attributes() {
use acadrust::entities::{AttributeEntity, EntityType, Insert};
use acadrust::types::Vector3;
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
let mut insert = Insert::new("A_CPT", Vector3::new(12.0, 34.0, 5.0));
insert.common.layer = "Equipment".to_string();
insert
.attributes
.push(AttributeEntity::simple("COMPANY", "BPH"));
insert
.attributes
.push(AttributeEntity::simple("STATUS", "ACTIVE"));
app.tabs[i].scene.add_entity(EntityType::Insert(insert));
let result = app
.automation_op(r#"{"op":"query","type":"Insert","layer":"Equipment","detail":"full"}"#);
assert_eq!(result["count"], 1);
assert_eq!(result["entities"][0]["type"], "Block Reference");
assert_eq!(result["entities"][0]["block"], "A_CPT");
assert_eq!(
result["entities"][0]["position"],
serde_json::json!([12.0, 34.0, 5.0])
);
assert_eq!(result["entities"][0]["attributes"]["COMPANY"], "BPH");
assert_eq!(result["entities"][0]["attributes"]["STATUS"], "ACTIVE");
assert_eq!(
result["entities"][0]["properties"]["attributes"][0]["value"],
"BPH"
);
let selected = app.automation_op(r#"{"op":"select","type":"Insert"}"#);
assert_eq!(selected["selected"], 1);
}
#[test] #[test]
fn ucs_interactive_inline_args() { fn ucs_interactive_inline_args() {
// `UCS Z 90` must drive the interactive UCS command step-by-step (option // `UCS Z 90` must drive the interactive UCS command step-by-step (option
@@ -800,9 +926,7 @@ mod tests {
fn tilted_ucs_places_planar_entities_with_the_plane_normal() { fn tilted_ucs_places_planar_entities_with_the_plane_normal() {
let mut app = OpenCADStudio::new_for_test(); let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#); app.automation_op(r#"{"op":"new"}"#);
app.automation_op( app.automation_op(r#"{"op":"run","cmd":"UCS 3POINT 0,0,0 1,0,0 0,0,1"}"#);
r#"{"op":"run","cmd":"UCS 3POINT 0,0,0 1,0,0 0,0,1"}"#,
);
app.automation_op(r#"{"op":"run","cmd":"CIRCLE 2,3 1"}"#); app.automation_op(r#"{"op":"run","cmd":"CIRCLE 2,3 1"}"#);
let circle = app.tabs[app.active_tab] let circle = app.tabs[app.active_tab]
@@ -842,7 +966,10 @@ mod tests {
h.linetype_scale h.linetype_scale
); );
// No command should be left dangling. // No command should be left dangling.
assert!(app.tabs[i].active_cmd.is_none(), "command must have finished"); assert!(
app.tabs[i].active_cmd.is_none(),
"command must have finished"
);
} }
#[test] #[test]
@@ -1004,7 +1131,9 @@ mod tests {
(20.0, 300.0), (20.0, 300.0),
] ]
}; };
let _ = app.update(Message::ViewportMove(iced::Point::new(path[0].0, path[0].1))); let _ = app.update(Message::ViewportMove(iced::Point::new(
path[0].0, path[0].1,
)));
let _ = app.update(Message::ViewportLeftPress); let _ = app.update(Message::ViewportLeftPress);
std::thread::sleep(std::time::Duration::from_millis(180)); std::thread::sleep(std::time::Duration::from_millis(180));
for &(x, y) in &path { for &(x, y) in &path {
@@ -1047,7 +1176,10 @@ mod tests {
// Two-step: bare command then the value, like typing 1 + Enter // Two-step: bare command then the value, like typing 1 + Enter
// (feed_active_cmd is the same path the GUI submit offers first). // (feed_active_cmd is the same path the GUI submit offers first).
let _ = app.run_command_line("PICKDRAG"); let _ = app.run_command_line("PICKDRAG");
assert!(app.tabs[app.active_tab].active_cmd.is_some(), "prompt must open"); assert!(
app.tabs[app.active_tab].active_cmd.is_some(),
"prompt must open"
);
let _ = app.feed_active_cmd("1"); let _ = app.feed_active_cmd("1");
assert!(app.pick_drag_rect, "PICKDRAG 1 via the prompt must switch"); assert!(app.pick_drag_rect, "PICKDRAG 1 via the prompt must switch");
} }
@@ -1085,10 +1217,7 @@ mod tests {
let _ = app.run_command_line("MATCHPROP"); let _ = app.run_command_line("MATCHPROP");
assert!(app.tabs[i].active_cmd.is_some(), "MATCHPROP must start"); assert!(app.tabs[i].active_cmd.is_some(), "MATCHPROP must start");
let _ = app.feed_command(StepInput::EntityPick(src_h, glam::DVec3::ZERO)); let _ = app.feed_command(StepInput::EntityPick(src_h, glam::DVec3::ZERO));
let _ = app.feed_command(StepInput::SelectionComplete(vec![ let _ = app.feed_command(StepInput::SelectionComplete(vec![dst_text_h, dst_mtext_h]));
dst_text_h,
dst_mtext_h,
]));
let doc = &app.tabs[i].scene.document; let doc = &app.tabs[i].scene.document;
match doc.get_entity(dst_text_h) { match doc.get_entity(dst_text_h) {
@@ -1140,7 +1269,10 @@ mod tests {
app.opening.is_none(), app.opening.is_none(),
"an already-open drawing must not start a load" "an already-open drawing must not start a load"
); );
assert!(app.pending_opens.is_empty(), "and must not queue one either"); assert!(
app.pending_opens.is_empty(),
"and must not queue one either"
);
// The same file spelled differently (a `..` hop) is still the same file. // The same file spelled differently (a `..` hop) is still the same file.
let indirect = canon.parent().unwrap().join("..").join( let indirect = canon.parent().unwrap().join("..").join(
@@ -1234,8 +1366,7 @@ mod tests {
assert_eq!(saved["ok"], true, "{label}: {}", saved["error"]); assert_eq!(saved["ok"], true, "{label}: {}", saved["error"]);
let saved_again = app.automation_op(r#"{"op":"save"}"#); let saved_again = app.automation_op(r#"{"op":"save"}"#);
assert_eq!( assert_eq!(
saved_again["ok"], saved_again["ok"], true,
true,
"normal save: {}", "normal save: {}",
saved_again["error"] saved_again["error"]
); );
@@ -1253,10 +1384,8 @@ mod tests {
#[test] #[test]
fn save_then_open_round_trips() { fn save_then_open_round_trips() {
let mut app = OpenCADStudio::new_for_test(); let mut app = OpenCADStudio::new_for_test();
let path = std::env::temp_dir().join(format!( let path =
"ocs_automation_test_{}.dxf", std::env::temp_dir().join(format!("ocs_automation_test_{}.dxf", std::process::id()));
std::process::id()
));
let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&path);
let p = path.to_string_lossy().replace('\\', "\\\\"); let p = path.to_string_lossy().replace('\\', "\\\\");
app.automation_op(r#"{"op":"new"}"#); app.automation_op(r#"{"op":"new"}"#);
@@ -1321,4 +1450,69 @@ mod tests {
drop(app); drop(app);
let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&path);
} }
#[test]
fn test_pline_line_then_arc() {
use crate::app::Message;
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
{
app.tabs[0].scene.selection.borrow_mut().vp_size = (1920.0, 1080.0);
app.tabs[0].scene.sync_tiles_from_panes(1920.0, 1080.0);
}
// Start PLINE
let _ = app.update(Message::CommandInput("PLINE".to_string()));
let _ = app.update(Message::CommandSubmit);
// Click first point (100, 100)
let _ = app.update(Message::ViewportMove(iced::Point::new(100.0, 100.0)));
let _ = app.update(Message::ViewportLeftPress);
let _ = app.update(Message::ViewportLeftRelease);
// Move to (200, 100) and click second point (draw line)
let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 100.0)));
let _ = app.update(Message::ViewportLeftPress);
let _ = app.update(Message::ViewportLeftRelease);
let wid = app.main_window.unwrap_or_else(iced::window::Id::unique);
// Switch to arc: Option 1 - CommandOptionPick("A")
let _ = app.update(Message::CommandOptionPick("A".to_string()));
let _ = app.view(wid);
println!(
"ENTITIES: {}",
app.tabs[0].scene.document.entities().count()
);
println!(
"CMD: {:?}",
app.tabs[0].active_cmd.as_ref().map(|c| c.name())
);
// Move mouse!
let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 100.0)));
let _ = app.view(wid);
let _ = app.update(Message::ViewportMove(iced::Point::new(201.0, 100.0)));
let _ = app.view(wid);
let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 150.0)));
let _ = app.view(wid);
let _ = app.update(Message::ViewportMove(iced::Point::new(150.0, 150.0)));
let _ = app.view(wid);
// Click arc point
let _ = app.update(Message::ViewportLeftPress);
let _ = app.update(Message::ViewportLeftRelease);
// Move mouse again
let _ = app.update(Message::ViewportMove(iced::Point::new(150.0, 160.0)));
// Switch to line
let _ = app.update(Message::CommandOptionPick("L".to_string()));
// Move mouse again
let _ = app.update(Message::ViewportMove(iced::Point::new(100.0, 150.0)));
// Finish
let _ = app.update(Message::CommandOptionPick(String::new()));
}
} }
+67 -4
View File
@@ -733,7 +733,10 @@ impl OpenCADStudio {
&result, &result,
CmdResult::Relaunch(..) CmdResult::Relaunch(..)
| CmdResult::Dispatch(..) | CmdResult::Dispatch(..)
| CmdResult::SolidEdgeBlend { .. }
| CmdResult::SolidSubtract { .. } | CmdResult::SolidSubtract { .. }
| CmdResult::SliceEntities { .. }
| CmdResult::SliceSurfaceEntities { .. }
); );
let task = self.apply_cmd_result_inner(result); let task = self.apply_cmd_result_inner(result);
let i = self.active_tab; let i = self.active_tab;
@@ -3091,6 +3094,14 @@ impl OpenCADStudio {
self.command_line.push_info(&prompt); self.command_line.push_info(&prompt);
} }
} }
CmdResult::ReportError(msg) => {
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.command_line.push_error(&msg);
if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()) {
self.command_line.push_info(&prompt);
}
}
CmdResult::ReportMeasurementAndDeselect(msg) => { CmdResult::ReportMeasurementAndDeselect(msg) => {
self.tabs[i].snap_result = None; self.tabs[i].snap_result = None;
self.tabs[i].scene.deselect_all(); self.tabs[i].scene.deselect_all();
@@ -4268,11 +4279,20 @@ impl OpenCADStudio {
CmdResult::SolidEdgeBlend { CmdResult::SolidEdgeBlend {
handle, handle,
pick, edges,
base_face,
value, value,
other_value,
fillet, fillet,
} => { } => {
let task = self.solid_edge_blend(handle, pick, value, fillet); let task = self.solid_edge_blend(
handle,
&edges,
base_face,
value,
other_value,
fillet,
);
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None; self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire(); self.tabs[i].scene.clear_preview_wire();
@@ -4280,8 +4300,51 @@ impl OpenCADStudio {
return task; return task;
} }
CmdResult::SolidSubtract { bases, cutters } => { CmdResult::SolidShell {
let task = self.solid_subtract(&bases, &cutters); handle,
actions,
distance,
} => {
let task = self.solid_shell(handle, &actions, distance);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
return task;
}
CmdResult::SolidSubtract {
bases,
cutters,
convert_meshes,
} => {
let task = self.solid_subtract(&bases, &cutters, convert_meshes);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
return task;
}
CmdResult::SliceEntities {
targets,
plane,
keep_point,
} => {
let task = self.solid_slice(&targets, plane, keep_point);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
return task;
}
CmdResult::SliceSurfaceEntities {
targets,
cutter,
keep_point,
} => {
let task = self.solid_slice_surface(&targets, *cutter, keep_point);
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None; self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire(); self.tabs[i].scene.clear_preview_wire();
+92 -12
View File
@@ -1,6 +1,6 @@
use super::*; use super::*;
fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> { fn selected_edge_body(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
let scene = &app.tabs.get(tab)?.scene; let scene = &app.tabs.get(tab)?.scene;
let selected = scene.selected_handles_in_order(); let selected = scene.selected_handles_in_order();
let [handle] = selected.as_slice() else { let [handle] = selected.as_slice() else {
@@ -9,7 +9,7 @@ fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
if matches!( if matches!(
scene.document.get_entity(*handle), scene.document.get_entity(*handle),
Some(acadrust::EntityType::Solid3D(_)) Some(acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Surface(_))
) { ) {
Some(*handle) Some(*handle)
} else { } else {
@@ -17,6 +17,36 @@ fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
} }
} }
fn solid_edge_sources(
app: &mut OpenCADStudio,
tab: usize,
) -> Vec<(acadrust::Handle, cadkernel::brep::Body)> {
let handles = app.tabs[tab]
.scene
.document
.entities()
.filter_map(|entity| {
matches!(
entity,
acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Surface(_)
)
.then_some(entity.common().handle)
})
.collect::<Vec<_>>();
app.tabs[tab].scene.restore_solid_models(&handles);
handles
.into_iter()
.filter_map(|handle| {
app.tabs[tab]
.scene
.solid_models
.get(&handle)
.cloned()
.map(|body| (handle, body))
})
.collect()
}
impl OpenCADStudio { impl OpenCADStudio {
pub(super) fn dispatch_dim(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> { pub(super) fn dispatch_dim(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
match cmd { match cmd {
@@ -960,16 +990,30 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(new_cmd)); self.tabs[i].active_cmd = Some(Box::new(new_cmd));
} }
"SOLIDFILLET" => { "FILLETEDGE" | "SOLIDFILLET" => {
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
let command = SolidEdgeCommand::new(EdgeOperation::Fillet, selected_solid(self, i)); let target = selected_edge_body(self, i);
let bodies = solid_edge_sources(self, i);
let command = SolidEdgeCommand::new(
EdgeOperation::Fillet,
target,
bodies,
crate::scene::WireModel::SELECTED,
);
self.command_line.push_info(&command.prompt()); self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command)); self.tabs[i].active_cmd = Some(Box::new(command));
} }
"FILLET" if selected_solid(self, i).is_some() => { "FILLET" if selected_edge_body(self, i).is_some() => {
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
let command = SolidEdgeCommand::new(EdgeOperation::Fillet, selected_solid(self, i)); let target = selected_edge_body(self, i);
let bodies = solid_edge_sources(self, i);
let command = SolidEdgeCommand::new(
EdgeOperation::Fillet,
target,
bodies,
crate::scene::WireModel::SELECTED,
);
self.command_line.push_info(&command.prompt()); self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command)); self.tabs[i].active_cmd = Some(Box::new(command));
} }
@@ -1107,16 +1151,52 @@ impl OpenCADStudio {
} }
} }
"SOLIDCHAMFER" => { "CHAMFEREDGE" | "SOLIDCHAMFER" => {
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; use crate::modules::model::edge_cmd::SolidEdgeCommand;
let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i)); let target = selected_edge_body(self, i);
let header = &self.tabs[i].scene.document.header;
let distances = (header.chamfer_distance_a, header.chamfer_distance_b);
let bodies = solid_edge_sources(self, i);
let command = SolidEdgeCommand::new_chamfer(
target,
bodies,
crate::scene::WireModel::SELECTED,
distances,
);
let distances = command.chamfer_distances();
self.command_line.push_output(
crate::tf!(
"Distance1 = {:.4}, Distance2 = {:.4}",
distances.0,
distances.1
)
.as_ref(),
);
self.command_line.push_info(&command.prompt()); self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command)); self.tabs[i].active_cmd = Some(Box::new(command));
} }
"CHAMFER" if selected_solid(self, i).is_some() => { "CHAMFER" if selected_edge_body(self, i).is_some() => {
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; use crate::modules::model::edge_cmd::SolidEdgeCommand;
let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i)); let target = selected_edge_body(self, i);
let header = &self.tabs[i].scene.document.header;
let distances = (header.chamfer_distance_a, header.chamfer_distance_b);
let bodies = solid_edge_sources(self, i);
let command = SolidEdgeCommand::new_chamfer(
target,
bodies,
crate::scene::WireModel::SELECTED,
distances,
);
let distances = command.chamfer_distances();
self.command_line.push_output(
crate::tf!(
"Distance1 = {:.4}, Distance2 = {:.4}",
distances.0,
distances.1
)
.as_ref(),
);
self.command_line.push_info(&command.prompt()); self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command)); self.tabs[i].active_cmd = Some(Box::new(command));
} }
+153 -64
View File
@@ -1082,54 +1082,95 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(new_cmd)); self.tabs[i].active_cmd = Some(Box::new(new_cmd));
} }
"SHELL" | "SOLIDEDIT" => {
use crate::modules::model::shell_cmd::ShellCommand;
let selected = self.tabs[i]
.scene
.selected_handles_in_order()
.into_iter()
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.filter(|handle| {
matches!(
self.tabs[i].scene.document.get_entity(*handle),
Some(acadrust::EntityType::Solid3D(_))
)
})
.collect::<Vec<_>>();
let target = (selected.len() == 1).then_some(selected[0]);
let new_cmd = if cmd == "SHELL" {
ShellCommand::direct(target)
} else {
ShellCommand::solid_edit(target)
};
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
// ── Solid booleans ───────────────────────────────────────────── // ── Solid booleans ─────────────────────────────────────────────
"UNION" | "INTERSECT" => { "UNION" => {
use crate::modules::model::boolean_cmd::BoolOp; use crate::modules::model::boolean_cmd::BoolOp;
if let Some(op) = BoolOp::from_id(cmd) { if self.union_ready() {
let solid_count = { return Some(self.solid_boolean(BoolOp::Union));
let scene = &self.tabs[i].scene; }
scene use crate::modules::draw::select::SelectObjectsCommand;
.selected_handles_in_order() let selection = SelectObjectsCommand::plain("UNION", "UNIONAPPLY");
.into_iter() self.command_line.push_info(&selection.prompt());
.filter(|handle| !scene.is_layer_locked(*handle)) self.tabs[i].active_cmd = Some(Box::new(selection));
.filter(|handle| { }
matches!(
scene.document.get_entity(*handle), "UNIONAPPLY" => {
Some(acadrust::EntityType::Solid3D(_)) use crate::modules::model::boolean_cmd::BoolOp;
) return Some(self.solid_boolean(BoolOp::Union));
}) }
.take(2)
.count() "INTERSECT" => {
}; use crate::modules::model::boolean_cmd::BoolOp;
if solid_count < 2 { if !self.intersect_ready() {
use crate::modules::draw::select::SelectObjectsCommand; use crate::modules::draw::select::SelectObjectsCommand;
let selection = SelectObjectsCommand::new(cmd); let selection = SelectObjectsCommand::new(cmd);
self.command_line.push_info(&selection.prompt()); self.command_line.push_info(&selection.prompt());
self.tabs[i].active_cmd = Some(Box::new(selection)); self.tabs[i].active_cmd = Some(Box::new(selection));
} else { } else {
return Some(self.solid_boolean(op)); return Some(self.solid_boolean(BoolOp::Intersect));
}
} }
} }
"SUBTRACT" => { "SUBTRACT" => {
use crate::modules::model::boolean_cmd::SubtractCommand; use crate::modules::model::boolean_cmd::SubtractCommand;
let bases = { let (bases, bases_have_mesh) = {
let scene = &self.tabs[i].scene; let scene = &self.tabs[i].scene;
scene let bases = scene
.selected_handles_in_order() .selected_handles_in_order()
.into_iter() .into_iter()
.filter(|handle| !scene.is_layer_locked(*handle)) .filter(|handle| !scene.is_layer_locked(*handle))
.filter(|handle| { .filter(|handle| {
matches!( matches!(
scene.document.get_entity(*handle), scene.document.get_entity(*handle),
Some(acadrust::EntityType::Solid3D(_)) Some(
acadrust::EntityType::Solid3D(_)
| acadrust::EntityType::Region(_)
| acadrust::EntityType::Surface(_)
| acadrust::EntityType::Mesh(_)
| acadrust::EntityType::PolygonMesh(_)
| acadrust::EntityType::PolyfaceMesh(_)
)
) )
}) })
.collect() .collect::<Vec<_>>();
let bases_have_mesh = bases.iter().any(|handle| {
matches!(
scene.document.get_entity(*handle),
Some(
acadrust::EntityType::Mesh(_)
| acadrust::EntityType::PolygonMesh(_)
| acadrust::EntityType::PolyfaceMesh(_)
)
)
});
(bases, bases_have_mesh)
}; };
self.tabs[i].scene.deselect_all(); self.tabs[i].scene.deselect_all();
let subtract = SubtractCommand::new(bases); let subtract = SubtractCommand::new(bases, bases_have_mesh);
self.command_line.push_info(&subtract.prompt()); self.command_line.push_info(&subtract.prompt());
self.tabs[i].active_cmd = Some(Box::new(subtract)); self.tabs[i].active_cmd = Some(Box::new(subtract));
} }
@@ -1180,9 +1221,9 @@ impl OpenCADStudio {
// REGION — convert selected closed boundaries (closed polylines / // REGION — convert selected closed boundaries (closed polylines /
// circles) into Region entities (one wire loop each). // circles) into Region entities (one wire loop each).
"REGION" | "REG" => { "REGION" | "REG" => {
use acadrust::entities::{Region, Wire}; use acadrust::entities::Region;
use acadrust::types::Vector3; use acadrust::types::Vector3;
let mut loops: Vec<Vec<Vector3>> = Vec::new(); let mut regions = Vec::new();
for (_, e) in self.tabs[i].scene.selected_entities().iter() { for (_, e) in self.tabs[i].scene.selected_entities().iter() {
let supported = matches!( let supported = matches!(
e, e,
@@ -1190,32 +1231,39 @@ impl OpenCADStudio {
if pl.is_closed && pl.vertices.len() >= 3 if pl.is_closed && pl.vertices.len() >= 3
) || matches!(e, acadrust::EntityType::Circle(_)); ) || matches!(e, acadrust::EntityType::Circle(_));
if supported { if supported {
let Some(curve) = crate::entities::curve::entity_curve(e) else { let Some((plane, loops, true)) =
crate::scene::model::presspull_model::profile_geometry(e)
else {
continue; continue;
}; };
loops.push( let Some(body) = cadkernel::brep::planar_region(plane, &loops) else {
crate::entities::curve::curve_points(&curve) continue;
.into_iter() };
.map(|point| Vector3::new(point[0], point[1], point[2])) let mut region = Region::new();
.collect(), region.point_of_reference = Vector3::new(
plane.origin[0],
plane.origin[1],
plane.origin[2],
); );
region.common.layer = self.tabs[i].active_layer.clone();
regions.push((region, body));
} }
} }
if loops.is_empty() { if regions.is_empty() {
self.command_line self.command_line
.push_error(crate::t!("REGION: select closed polylines or circles.").as_ref()); .push_error(crate::t!("REGION: select closed polylines or circles.").as_ref());
} else { } else {
self.push_undo_snapshot(i, "REGION"); self.push_undo_snapshot(i, "REGION");
let count = loops.len(); let count = regions.len();
for pts in loops { let mut created = Vec::with_capacity(count);
let mut w = Wire::new(); for (region, body) in regions {
let first = pts.first().copied().unwrap_or(Vector3::new(0.0, 0.0, 0.0)); let handle = self.add_region_model(region, body);
w.points = pts; if handle.is_null() {
let mut r = Region::new(); self.tabs[i].scene.rollback_new_entities(&created);
r.point_of_reference = first; self.discard_last_undo_entry(i);
r.wires = vec![w]; return Some(iced::Task::none());
r.common.layer = self.tabs[i].active_layer.clone(); }
self.tabs[i].scene.add_entity(acadrust::EntityType::Region(r)); created.push(handle);
} }
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.command_line self.command_line
@@ -1370,21 +1418,51 @@ impl OpenCADStudio {
} }
} }
// SLICE [X|Y|Z] <value> [TOP|BOTTOM] — cut the selected solid with an
// axis-aligned plane, keeping the lower half by default.
"SLICE" | "SL" => { "SLICE" | "SL" => {
use crate::command::SelectThenKeywordCommand; use crate::modules::model::slice_cmd::SliceCommand;
let has_sel = !self.tabs[i].scene.selected_entities().is_empty(); let (targets, centre, radius, view_normal) = {
let c = SelectThenKeywordCommand::new( let scene = &mut self.tabs[i].scene;
"SLICE", let mut targets = scene
"SLICE cutting-plane axis [X / Y / Z] (add TOP/BOTTOM by typing):", .selected_handles_in_order()
vec![ .into_iter()
("X", "X", Some("SLICE offset along X:")), .filter(|handle| !scene.is_layer_locked(*handle))
("Y", "Y", Some("SLICE offset along Y:")), .filter(|handle| {
("Z", "Z", Some("SLICE offset along Z:")), matches!(
], scene.document.get_entity(*handle),
has_sel, Some(
); acadrust::EntityType::Solid3D(_)
| acadrust::EntityType::Surface(_)
)
)
})
.collect::<Vec<_>>();
scene.restore_solid_models(&targets);
targets.retain(|handle| scene.solid_models.contains_key(handle));
let bounds = targets
.iter()
.filter_map(|handle| {
crate::scene::model::solid_model::extent(&scene.solid_models[handle])
})
.fold(None::<([f64; 3], [f64; 3])>, |bounds, (low, high)| {
Some(match bounds {
None => (low, high),
Some((mut min, mut max)) => {
for axis in 0..3 {
min[axis] = min[axis].min(low[axis]);
max[axis] = max[axis].max(high[axis]);
}
(min, max)
}
})
});
let (centre, radius) = bounds.map_or((glam::DVec3::ZERO, 10.0), |(min, max)| {
let min = glam::DVec3::from_array(min);
let max = glam::DVec3::from_array(max);
((min + max) * 0.5, (max - min).length().max(2.0) * 0.65)
});
(targets, centre, radius, scene.active_gaze_dir().as_dvec3())
};
let c = SliceCommand::new(targets, view_normal, centre, radius);
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));
} }
@@ -1403,7 +1481,18 @@ impl OpenCADStudio {
let value: Option<f64> = parts.get(val_idx).and_then(|s| s.parse().ok()); let value: Option<f64> = parts.get(val_idx).and_then(|s| s.parse().ok());
let keep_low = !parts.iter().any(|s| s == "TOP"); let keep_low = !parts.iter().any(|s| s == "TOP");
match value { match value {
Some(v) => return Some(self.solid_slice(axis, v, keep_low)), Some(v) => {
let (origin, x, normal) = match axis {
0 => ([v, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]),
1 => ([0.0, v, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]),
_ => ([0.0, 0.0, v], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]),
};
let plane = cadkernel::space::Plane::orthonormal(origin, x, normal)
.expect("fixed world axes define a plane");
let side = glam::DVec3::from_array(origin)
+ glam::DVec3::from_array(normal) * if keep_low { -1.0 } else { 1.0 };
return Some(self.slice_selected(plane, Some(side)));
}
None => self.command_line.push_info( None => self.command_line.push_info(
crate::t!("Usage: SLICE [X|Y|Z] <value> [TOP|BOTTOM] (cuts the selected solid)").as_ref(), crate::t!("Usage: SLICE [X|Y|Z] <value> [TOP|BOTTOM] (cuts the selected solid)").as_ref(),
), ),
+4 -6
View File
@@ -335,9 +335,9 @@ impl OpenCADStudio {
"PERSP" => return Some(Task::done(Message::SetProjection(false))), "PERSP" => return Some(Task::done(Message::SetProjection(false))),
"LAYERS" => return Some(Task::done(Message::ToggleLayers)), "LAYERS" => return Some(Task::done(Message::ToggleLayers)),
// SCRIPT <path> — run a command script: each non-blank, non-comment // SCRIPT <path> — run a command script: each non-comment
// line is fed through the same command path the `--script` startup // line is fed through the same command path the `--script` startup
// flag uses, so the behaviour matches headless automation exactly. // flag uses. Blank rows submit Enter to the active command.
"SCRIPT" | "SCR" => { "SCRIPT" | "SCR" => {
use crate::command::ValuePromptCommand; use crate::command::ValuePromptCommand;
let c = ValuePromptCommand::new("SCRIPT", "SCRIPT path to the .scr file:"); let c = ValuePromptCommand::new("SCRIPT", "SCRIPT path to the .scr file:");
@@ -352,10 +352,8 @@ impl OpenCADStudio {
let cmds: Vec<Task<Message>> = text let cmds: Vec<Task<Message>> = text
.lines() .lines()
.map(str::trim) .map(str::trim)
.filter(|l| { .filter(|l| !l.starts_with('#') && !l.starts_with(';'))
!l.is_empty() && !l.starts_with('#') && !l.starts_with(';') .map(|l| Task::done(Message::ScriptLine(l.to_string())))
})
.map(|l| Task::done(Message::Command(l.to_string())))
.collect(); .collect();
self.command_line.push_output(crate::tf!( self.command_line.push_output(crate::tf!(
"SCRIPT: running {} command(s) from {p}.", "SCRIPT: running {} command(s) from {p}.",
+115 -13
View File
@@ -2,7 +2,7 @@
use super::{Message, OpenCADStudio}; use super::{Message, OpenCADStudio};
use crate::command::{InputKind, StepInput}; use crate::command::{InputKind, StepInput};
use iced::Task; use iced::Task;
use serde_json::{json, Value}; use serde_json::{Value, json};
use std::{collections::VecDeque, sync::OnceLock}; use std::{collections::VecDeque, sync::OnceLock};
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
mod transport; mod transport;
@@ -361,7 +361,7 @@ impl OpenCADStudio {
"mtext_editor":self.mtext_editor.as_ref().map(|e|json!({"text":e.content.text(),"height":e.height,"style":e.style})), "mtext_editor":self.mtext_editor.as_ref().map(|e|json!({"text":e.content.text(),"height":e.height,"style":e.style})),
"text_editor":self.text_inline.is_some(),"event_cursor":self.control.serial, "text_editor":self.text_inline.is_some(),"event_cursor":self.control.serial,
"operation":self.control.pending.as_ref().map(|p| &p.id), "operation":self.control.pending.as_ref().map(|p| &p.id),
"capabilities":["commands","command_manifest","step_input","batch","compact_results","entity_pick","structure_pick","selection","properties","layers","history","documents","events","capture","viewport_capture","measure","spatial_query"] "capabilities":["commands","command_manifest","step_input","batch","compact_results","entity_pick","structure_pick","selection","properties","records","record_schemas","record_filters","atomic_record_updates","layers","history","documents","events","capture","viewport_capture","measure","spatial_query"]
}) })
} }
@@ -377,6 +377,9 @@ impl OpenCADStudio {
| "properties" | "properties"
| "measure" | "measure"
| "query" | "query"
| "records"
| "record_schema"
| "capabilities"
| "entities" | "entities"
| "layers" | "layers"
| "header" | "header"
@@ -449,7 +452,9 @@ impl OpenCADStudio {
return ( return (
failure( failure(
"unknown_command", "unknown_command",
format!("Unknown command {requested}; call commands without name to list commands"), format!(
"Unknown command {requested}; call commands without name to list commands"
),
), ),
Task::none(), Task::none(),
); );
@@ -511,7 +516,7 @@ impl OpenCADStudio {
"Supply a unique request_id (maximum 128 bytes)", "Supply a unique request_id (maximum 128 bytes)",
), ),
Task::none(), Task::none(),
) );
} }
}; };
if let Some((_, old, result)) = self.control.completed.iter().find(|(i, _, _)| i == &id) if let Some((_, old, result)) = self.control.completed.iter().find(|(i, _, _)| i == &id)
@@ -773,6 +778,7 @@ impl OpenCADStudio {
Task::none() Task::none()
} }
"property" => self.control_set_property(req)?, "property" => self.control_set_property(req)?,
"set_properties" => self.control_set_record_properties(req)?,
"action" => self.control_ui_action(req)?, "action" => self.control_ui_action(req)?,
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
"save" => { "save" => {
@@ -814,6 +820,12 @@ impl OpenCADStudio {
}) })
} }
pub(super) fn set_control_result(&mut self, value: Value) {
if let Some(operation) = self.control.pending.as_mut() {
operation.result = value;
}
}
pub(super) fn control_track(&mut self, task: Task<Message>) -> Task<Message> { pub(super) fn control_track(&mut self, task: Task<Message>) -> Task<Message> {
if task.units() == 0 { if task.units() == 0 {
return task; return task;
@@ -982,7 +994,7 @@ impl OpenCADStudio {
"area":planar.curve.is_closed().then(|| planar.curve.enclosed_area().abs()) "area":planar.curve.is_closed().then(|| planar.curve.enclosed_area().abs())
}) })
}); });
out.push(json!({"handle":format!("{:X}",handle.value()),"type":crate::entities::names::ui_name(entity),"bounds":{"min":[bb.min.x,bb.min.y,bb.min.z],"max":[bb.max.x,bb.max.y,bb.max.z]},"curve":curve,"mesh":metrics.map(|m|json!({"vertices":m.metrics.vertices,"triangles":m.metrics.triangles,"surface_area":m.metrics.surface_area,"volume":m.metrics.volume,"centroid":m.metrics.centroid}))})); out.push(json!({"handle":format!("{:X}",handle.value()),"type":crate::entities::names::ui_name(entity),"bounds":{"min":[bb.min.x,bb.min.y,bb.min.z],"max":[bb.max.x,bb.max.y,bb.max.z]},"curve":curve,"mesh":metrics.map(|m|json!({"vertices":m.metrics.vertices,"triangles":m.metrics.triangles,"surface_area":m.metrics.surface_area,"volume":m.metrics.volume,"centroid":m.metrics.centroid,"moment_of_inertia":m.metrics.moment_of_inertia,"principal_directions":m.metrics.principal_directions,"principal_moments":m.metrics.principal_moments,"product_of_inertia":m.metrics.product_of_inertia,"radii_of_gyration":m.metrics.radii_of_gyration}))}));
} }
json!({"ok":true,"document_id":tab.id,"geometry_revision":tab.scene.geometry_epoch,"measurements":out}) json!({"ok":true,"document_id":tab.id,"geometry_revision":tab.scene.geometry_epoch,"measurements":out})
} }
@@ -1101,10 +1113,12 @@ mod tests {
); );
let started = request(&mut app, json!({"op":"start","cmd":"LINE"})); let started = request(&mut app, json!({"op":"start","cmd":"LINE"}));
assert_eq!(started["status"], "waiting_input"); assert_eq!(started["status"], "waiting_input");
assert!(started["state"]["command"]["accepts"] assert!(
.as_array() started["state"]["command"]["accepts"]
.unwrap() .as_array()
.contains(&json!("point"))); .unwrap()
.contains(&json!("point"))
);
assert_eq!( assert_eq!(
started["state"]["command"]["input_example"]["kind"], started["state"]["command"]["input_example"]["kind"],
"point" "point"
@@ -1129,10 +1143,12 @@ mod tests {
); );
assert_eq!(app.automation_op(r#"{"op":"entities"}"#)["total"], 1); assert_eq!(app.automation_op(r#"{"op":"entities"}"#)["total"], 1);
request(&mut app, json!({"op":"select","type":"LINE"})); request(&mut app, json!({"op":"select","type":"LINE"}));
assert!(!app.control_properties()["sections"] assert!(
.as_array() !app.control_properties()["sections"]
.unwrap() .as_array()
.is_empty()); .unwrap()
.is_empty()
);
let changed = request(&mut app, json!({"op":"property","field":"color","value":1})); let changed = request(&mut app, json!({"op":"property","field":"color","value":1}));
assert_eq!(changed["ok"], true, "{changed}"); assert_eq!(changed["ok"], true, "{changed}");
assert_eq!( assert_eq!(
@@ -1222,6 +1238,92 @@ mod tests {
assert!((curve["length"].as_f64().unwrap() - std::f64::consts::TAU * 2.0).abs() < 1e-9); assert!((curve["length"].as_f64().unwrap() - std::f64::consts::TAU * 2.0).abs() < 1e-9);
assert!((curve["area"].as_f64().unwrap() - std::f64::consts::PI * 4.0).abs() < 1e-9); assert!((curve["area"].as_f64().unwrap() - std::f64::consts::PI * 4.0).abs() < 1e-9);
} }
#[test]
fn control_shell_roundtrips_with_kernel_mass_properties() {
let mut app = OpenCADStudio::new_for_test();
request(&mut app, json!({"op":"new"}));
request(&mut app, json!({"op":"start","cmd":"BOX"}));
request(
&mut app,
json!({"op":"input","kind":"point","point":[0.0,0.0,0.0]}),
);
request(
&mut app,
json!({"op":"input","kind":"point","point":[6.0,5.0,0.0]}),
);
assert_eq!(
request(&mut app, json!({"op":"input","kind":"token","text":"4"}))["status"],
"completed"
);
let handle = app.tabs[app.active_tab]
.scene
.document
.entities()
.find_map(|entity| {
matches!(entity, acadrust::EntityType::Solid3D(_)).then(|| entity.common().handle)
})
.unwrap();
let handle_text = format!("{:X}", handle.value());
request(&mut app, json!({"op":"select","handles":[handle_text]}));
request(&mut app, json!({"op":"start","cmd":"SHELL"}));
request(
&mut app,
json!({"op":"input","kind":"entity","handle":handle_text,"point":[3.0,2.5,4.0]}),
);
request(&mut app, json!({"op":"input","kind":"enter"}));
assert_eq!(
request(&mut app, json!({"op":"input","kind":"token","text":"0.5"}))["status"],
"completed"
);
let measured = app.control_measure(&json!({"handles":[handle_text]}));
let mesh = &measured["measurements"][0]["mesh"];
assert!((mesh["volume"].as_f64().unwrap() - 50.0).abs() < 1e-9);
assert_eq!(mesh["principal_moments"].as_array().unwrap().len(), 3);
assert!(matches!(
app.tabs[app.active_tab]
.scene
.document
.solid_history_operation(handle),
Some(acadrust::objects::SolidHistoryOperation::Brep(_))
));
assert_eq!(request(&mut app, json!({"op":"undo"}))["ok"], true);
let restored = app.control_measure(&json!({"handles":[handle_text]}));
assert!(
(restored["measurements"][0]["mesh"]["volume"]
.as_f64()
.unwrap()
- 120.0)
.abs()
< 1e-9
);
assert_eq!(request(&mut app, json!({"op":"redo"}))["ok"], true);
let path = std::env::temp_dir().join(format!("ocs-shell-{}.dwg", session_id()));
assert_eq!(
request(&mut app, json!({"op":"save","path":path}))["ok"],
true
);
request(&mut app, json!({"op":"new"}));
assert_eq!(
request(&mut app, json!({"op":"open","path":path}))["status"],
"completed"
);
let reopened = app.control_measure(&json!({"handles":[handle_text]}));
assert!(
(reopened["measurements"][0]["mesh"]["volume"]
.as_f64()
.unwrap()
- 50.0)
.abs()
< 1e-9
);
let _ = std::fs::remove_file(path);
}
#[test] #[test]
fn control_retry_is_idempotent_and_stale_state_rejected() { fn control_retry_is_idempotent_and_stale_state_rejected() {
let mut app = OpenCADStudio::new_for_test(); let mut app = OpenCADStudio::new_for_test();
+1 -9
View File
@@ -999,14 +999,6 @@ fn kernel_placement(p: &PlacementSpec) -> cadkernel::brep::Placement {
} }
} }
/// Signed volume and centroid of a closed triangle mesh via the divergence
/// theorem: V = Σ v0·(v1×v2)/6, C = Σ (v0+v1+v2)·tetra_vol / (4V). Single source
/// of truth for both (used by the cold-cache `volume`/`centroid` fallback).
fn mesh_volume_centroid(mesh: &cadkernel::brep::Mesh) -> (f64, [f64; 3]) {
// Delegate to the crate's single source of truth (divergence theorem).
ocs_doc_api::geom::mesh_volume_centroid(mesh)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -3235,7 +3227,7 @@ mod tests {
let body = crate::scene::convert::solid3d_tess::kernel_body(solid) let body = crate::scene::convert::solid3d_tess::kernel_body(solid)
.expect("re-lift reloaded solid failed"); .expect("re-lift reloaded solid failed");
let mesh = cadkernel::brep::mesh_body(&body, 0.5, 1e-3); let mesh = cadkernel::brep::mesh_body(&body, 0.5, 1e-3);
let vol = mesh_volume_centroid(&mesh).0; let vol = ocs_doc_api::geom::mesh_volume_centroid(&mesh).0;
assert!( assert!(
(vol - 125.0).abs() < 1.0, (vol - 125.0).abs() < 1.0,
"reloaded intersection volume {vol}" "reloaded intersection volume {vol}"
+73 -60
View File
@@ -9,21 +9,22 @@ pub(crate) mod config;
pub use automation::{export_headless, serve}; pub use automation::{export_headless, serve};
mod command_driver; mod command_driver;
pub(crate) mod commands; pub(crate) mod commands;
#[cfg(not(target_arch = "wasm32"))]
mod doc_api;
mod document; mod document;
pub(crate) mod expr_eval; pub(crate) mod expr_eval;
mod find_replace; mod find_replace;
pub(crate) mod helpers; pub(crate) mod helpers;
mod history; mod history;
#[cfg(not(target_arch = "wasm32"))]
mod doc_api;
mod layers; mod layers;
mod model_ops; mod model_ops;
mod mtext_editor; mod mtext_editor;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub mod plugin_host; pub mod plugin_host;
mod properties;
mod presspull_ops; mod presspull_ops;
mod properties;
mod recent; mod recent;
mod record_api;
pub(crate) mod settings; pub(crate) mod settings;
mod shortcuts; mod shortcuts;
mod startup; mod startup;
@@ -225,7 +226,10 @@ impl From<&QSelectState> for QSelectSettings {
Self { Self {
scope: state.scope, scope: state.scope,
type_filter: state.type_filter.clone(), type_filter: state.type_filter.clone(),
property_field: state.property.as_ref().map(|property| property.field.clone()), property_field: state
.property
.as_ref()
.map(|property| property.field.clone()),
operator: state.operator, operator: state.operator,
value: state.value.clone(), value: state.value.clone(),
mode: state.mode, mode: state.mode,
@@ -256,12 +260,12 @@ pub(crate) enum FindMatchKey {
} }
use crate::snap::Snapper; use crate::snap::Snapper;
use crate::ui::{CommandLine, Ribbon, StatusBar}; use crate::ui::{CommandLine, Ribbon, StatusBar};
use acadrust::types::{Color as AcadColor, LineWeight};
use acadrust::CadDocument; use acadrust::CadDocument;
use acadrust::types::{Color as AcadColor, LineWeight};
use iced::time::Instant; use iced::time::Instant;
use iced::window; use iced::window;
use iced::{mouse, Point, Task, Theme}; use iced::{Point, Task, Theme, mouse};
use std::sync::Arc; use std::sync::Arc;
pub(super) const POLY_START_DELAY_MS: u128 = 150; pub(super) const POLY_START_DELAY_MS: u128 = 150;
@@ -318,9 +322,7 @@ struct AddSelectedRestore {
} }
/// Which Start-page section a narrow (tabbed) Start page is showing. /// Which Start-page section a narrow (tabbed) Start page is showing.
#[derive( #[derive(Clone, Copy, PartialEq, Eq, Default, Debug, serde::Serialize, serde::Deserialize)]
Clone, Copy, PartialEq, Eq, Default, Debug, serde::Serialize, serde::Deserialize,
)]
pub enum StartSection { pub enum StartSection {
Recent, Recent,
Videos, Videos,
@@ -780,15 +782,13 @@ pub(super) struct OpenCADStudio {
/// Live filter for installed and available plugin cards. /// Live filter for installed and available plugin cards.
plugin_search_input: String, plugin_search_input: String,
/// Installable release tags fetched per linked repo (for the dropdown). /// Installable release tags fetched per linked repo (for the dropdown).
repo_release_tags: repo_release_tags: rustc_hash::FxHashMap<String, Vec<crate::plugin::external::ReleaseInfo>>,
rustc_hash::FxHashMap<String, Vec<crate::plugin::external::ReleaseInfo>>,
/// The release tag currently selected per linked repo. /// The release tag currently selected per linked repo.
repo_selected_tag: rustc_hash::FxHashMap<String, String>, repo_selected_tag: rustc_hash::FxHashMap<String, String>,
/// Repository currently shown in the Plugin Manager detail pane. /// Repository currently shown in the Plugin Manager detail pane.
selected_plugin_repo: Option<String>, selected_plugin_repo: Option<String>,
/// Parsed GitHub README content or the last fetch error, cached per repo. /// Parsed GitHub README content or the last fetch error, cached per repo.
plugin_readmes: plugin_readmes: rustc_hash::FxHashMap<String, Result<iced::widget::markdown::Content, String>>,
rustc_hash::FxHashMap<String, Result<iced::widget::markdown::Content, String>>,
/// README requests in flight, used to render a deterministic loading state. /// README requests in flight, used to render a deterministic loading state.
plugin_readme_loading: rustc_hash::FxHashSet<String>, plugin_readme_loading: rustc_hash::FxHashSet<String>,
/// Last marketplace status / error line shown in the Plugin Manager. /// Last marketplace status / error line shown in the Plugin Manager.
@@ -1098,8 +1098,7 @@ pub(super) struct OpenCADStudio {
save_job_serial: u64, save_job_serial: u64,
/// Destination leases held while Save As workers are active. /// Destination leases held while Save As workers are active.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pending_save_leases: pending_save_leases: std::collections::HashMap<u64, crate::io::edit_lock::EditLease>,
std::collections::HashMap<u64, crate::io::edit_lock::EditLease>,
/// Locked-file failure currently shown in the recovery dialog. /// Locked-file failure currently shown in the recovery dialog.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pending_save_failure: Option<PendingSaveFailure>, pending_save_failure: Option<PendingSaveFailure>,
@@ -1448,7 +1447,8 @@ impl ClipboardDeps {
if !objects.is_empty() { if !objects.is_empty() {
let mut annotation_scales = Vec::new(); let mut annotation_scales = Vec::new();
for (_, object) in &objects { for (_, object) in &objects {
let acadrust::objects::ObjectType::ObjectContextData(context) = object else { let acadrust::objects::ObjectType::ObjectContextData(context) = object
else {
continue; continue;
}; };
if annotation_scales if annotation_scales
@@ -1618,9 +1618,7 @@ impl ClipboardDeps {
fn snapshot_block(doc: &acadrust::CadDocument, name: &str) -> Option<BlockDef> { fn snapshot_block(doc: &acadrust::CadDocument, name: &str) -> Option<BlockDef> {
use acadrust::EntityType; use acadrust::EntityType;
let br = doc.block_records.get(name)?; let br = doc.block_records.get(name)?;
if name.starts_with("*Model_Space") if name.starts_with("*Model_Space") || name.starts_with("*Paper_Space") || br.flags.is_xref
|| name.starts_with("*Paper_Space")
|| br.flags.is_xref
{ {
return None; return None;
} }
@@ -1835,10 +1833,7 @@ pub enum Message {
/// Register a font already held by the shared web store with the UI renderer. /// Register a font already held by the shared web store with the UI renderer.
ApplyWebFont(crate::scene::text::web_font::Script), ApplyWebFont(crate::scene::text::web_font::Script),
/// Completion of the UI renderer's runtime font registration. /// Completion of the UI renderer's runtime font registration.
WebUiFontLoaded( WebUiFontLoaded(crate::scene::text::web_font::Script, Result<(), String>),
crate::scene::text::web_font::Script,
Result<(), String>,
),
/// Ctrl+V. Routed by `update` into an open text editor, the drawing-object /// Ctrl+V. Routed by `update` into an open text editor, the drawing-object
/// clipboard, or the system text clipboard. /// clipboard, or the system text clipboard.
PasteShortcut, PasteShortcut,
@@ -1906,10 +1901,13 @@ pub enum Message {
WebFileOpened(u64, crate::io::WebOpenOutcome), WebFileOpened(u64, crate::io::WebOpenOutcome),
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
WebFileCached(u64, crate::io::WebOpenOutcome, Result<(), String>), WebFileCached(u64, crate::io::WebOpenOutcome, Result<(), String>),
FileOpened(u64, Result< FileOpened(
(String, PathBuf, CadDocument, crate::scene::DerivedCaches), u64,
crate::io::OpenLoadError, Result<
>), (String, PathBuf, CadDocument, crate::scene::DerivedCaches),
crate::io::OpenLoadError,
>,
),
RecoveryClose, RecoveryClose,
RecoveryAttempt, RecoveryAttempt,
RecoveryDecline, RecoveryDecline,
@@ -2121,6 +2119,10 @@ pub enum Message {
CommandInput(String), CommandInput(String),
CommandSubmit, CommandSubmit,
Command(String), Command(String),
/// Execute one complete line read from a command script. Unlike UI/ribbon
/// dispatch, this accepts an interactive verb and all of its arguments on
/// the same line (`BOX 0,0,0 10,10,0 10`).
ScriptLine(String),
/// Append one typed character to the command-line input from the /// Append one typed character to the command-line input from the
/// global key-press subscription. Used when the text-input widget /// global key-press subscription. Used when the text-input widget
/// itself isn't focused (focus parked on viewport / button / etc.) /// itself isn't focused (focus parked on viewport / button / etc.)
@@ -2150,9 +2152,16 @@ pub enum Message {
}, },
/// A widget captured Up/Down; resolve it only if the command input owns /// A widget captured Up/Down; resolve it only if the command input owns
/// keyboard focus. /// keyboard focus.
CommandLineArrowProbe { direction: ArrowKey, extend_selection: bool }, CommandLineArrowProbe {
direction: ArrowKey,
extend_selection: bool,
},
/// Result of the command-input focus query for a captured Up/Down key. /// Result of the command-input focus query for a captured Up/Down key.
CommandLineArrowResolved { direction: ArrowKey, focused: bool, extend_selection: bool }, CommandLineArrowResolved {
direction: ArrowKey,
focused: bool,
extend_selection: bool,
},
/// Toggle the dropdown listing the full command-line history. /// Toggle the dropdown listing the full command-line history.
CommandHistoryToggle, CommandHistoryToggle,
/// Grab/move/release the expanded history panel's top resize edge. /// Grab/move/release the expanded history panel's top resize edge.
@@ -2303,9 +2312,7 @@ pub enum Message {
epoch: u64, epoch: u64,
source: usize, source: usize,
wires: std::sync::Weak<Vec<crate::scene::WireModel>>, wires: std::sync::Weak<Vec<crate::scene::WireModel>>,
index: std::sync::Arc< index: std::sync::Arc<crate::scene::pick::interaction_index::InteractionIndex>,
crate::scene::pick::interaction_index::InteractionIndex,
>,
build_ms: f64, build_ms: f64,
}, },
WindowResized(f32, f32), WindowResized(f32, f32),
@@ -2567,7 +2574,10 @@ pub enum Message {
PropEditChoiceToggle, PropEditChoiceToggle,
/// User is typing in a block-attribute value field (live buffer update), /// User is typing in a block-attribute value field (live buffer update),
/// keyed by the attribute tag. /// keyed by the attribute tag.
PropAttrInput { tag: String, value: String }, PropAttrInput {
tag: String,
value: String,
},
/// User committed a block-attribute value edit (Enter pressed). /// User committed a block-attribute value edit (Enter pressed).
PropAttrCommit(String), PropAttrCommit(String),
/// Reports the currently keyboard-focused widget (if any) after a /// Reports the currently keyboard-focused widget (if any) after a
@@ -2593,7 +2603,10 @@ pub enum Message {
PropColorFieldToggle(String), PropColorFieldToggle(String),
/// User picked a colour for a generic per-field colour row (hatch gradient /// User picked a colour for a generic per-field colour row (hatch gradient
/// `Color 1` / `Color 2`), routed by the field name. /// `Color 1` / `Color 2`), routed by the field name.
PropColorFieldChanged { field: String, color: AcadColor }, PropColorFieldChanged {
field: String,
color: AcadColor,
},
/// Collapse the inline color picker dropdown. Fired when another /// Collapse the inline color picker dropdown. Fired when another
/// properties-panel dropdown (a combo_box) opens, so at most one panel /// properties-panel dropdown (a combo_box) opens, so at most one panel
/// dropdown is open at a time and they can't overlap. (#235) /// dropdown is open at a time and they can't overlap. (#235)
@@ -2705,7 +2718,10 @@ pub enum Message {
/// Select the row the Text Options / Properties tabs act on. /// Select the row the Text Options / Properties tabs act on.
AttrEditorSelect(usize), AttrEditorSelect(usize),
/// Live edit of the attribute value at row `idx` (Attribute tab). /// Live edit of the attribute value at row `idx` (Attribute tab).
AttrEditorInput { idx: usize, value: String }, AttrEditorInput {
idx: usize,
value: String,
},
// Text Options — all act on the selected row: // Text Options — all act on the selected row:
AttrEditorTextStyle(String), AttrEditorTextStyle(String),
AttrEditorJustify(String), AttrEditorJustify(String),
@@ -2760,9 +2776,7 @@ pub enum Message {
/// GitHub Discussions fetched at boot for the Start page. /// GitHub Discussions fetched at boot for the Start page.
DiscussionsFetched(Result<Vec<crate::discussions::DiscussionEntry>, String>), DiscussionsFetched(Result<Vec<crate::discussions::DiscussionEntry>, String>),
/// Recent-file DWG preview thumbnails decoded on a background thread. /// Recent-file DWG preview thumbnails decoded on a background thread.
RecentThumbsLoaded( RecentThumbsLoaded(Vec<(std::path::PathBuf, Option<iced::widget::image::Handle>)>),
Vec<(std::path::PathBuf, Option<iced::widget::image::Handle>)>,
),
/// Installable releases and manifest API versions fetched for `owner/repo`. /// Installable releases and manifest API versions fetched for `owner/repo`.
PluginReleasesFetched( PluginReleasesFetched(
String, String,
@@ -2803,7 +2817,10 @@ pub enum Message {
InvertSelection, InvertSelection,
/// Keyboard modifier state changed — tracks whether Shift is held so the /// Keyboard modifier state changed — tracks whether Shift is held so the
/// pick path can do subtractive (Shift+click) selection. /// pick path can do subtractive (Shift+click) selection.
SetModifiers { shift: bool, ctrl: bool }, SetModifiers {
shift: bool,
ctrl: bool,
},
// ── In-place MText editor ─────────────────────────────────────────── // ── In-place MText editor ───────────────────────────────────────────
/// Text-area edit action from the multi-line editor widget. /// Text-area edit action from the multi-line editor widget.
MTextEdit(iced::widget::text_editor::Action), MTextEdit(iced::widget::text_editor::Action),
@@ -3273,10 +3290,7 @@ impl OpenCADStudio {
.iter() .iter()
.filter_map(|v| { .filter_map(|v| {
let bytes = v.thumb.clone()?; let bytes = v.thumb.clone()?;
Some(( Some((v.id.clone(), iced::widget::image::Handle::from_bytes(bytes)))
v.id.clone(),
iced::widget::image::Handle::from_bytes(bytes),
))
}) })
.collect(); .collect();
self.videos = videos; self.videos = videos;
@@ -3790,8 +3804,7 @@ impl OpenCADStudio {
let state = Self::new(); let state = Self::new();
let (id, open_task) = window::open(window::Settings { let (id, open_task) = window::open(window::Settings {
maximized: true, maximized: true,
icon: build_window_icon() icon: build_window_icon().and_then(|rgba| window::icon::from_rgba(rgba, 32, 32).ok()),
.and_then(|rgba| window::icon::from_rgba(rgba, 32, 32).ok()),
exit_on_close_request: false, exit_on_close_request: false,
// A Wayland compositor has no StartupWMClass to go on: it resolves a // A Wayland compositor has no StartupWMClass to go on: it resolves a
// window's dock icon by matching the window's app_id against the // window's dock icon by matching the window's app_id against the
@@ -3844,7 +3857,7 @@ impl OpenCADStudio {
Task::batch( Task::batch(
cfg.script_lines cfg.script_lines
.into_iter() .into_iter()
.map(|line| Task::done(Message::Command(line))), .map(|line| Task::done(Message::ScriptLine(line))),
) )
}; };
s.queue_startup_prompts(); s.queue_startup_prompts();
@@ -3927,24 +3940,17 @@ impl OpenCADStudio {
let mut s = Self::new(); let mut s = Self::new();
s.queue_startup_prompts(); s.queue_startup_prompts();
let focus = s.focus_cmd_input(); let focus = s.focus_cmd_input();
let primary_font = crate::scene::text::web_font::preload_language( let primary_font =
&crate::i18n::active_language_tag(), crate::scene::text::web_font::preload_language(&crate::i18n::active_language_tag());
);
let fonts = Task::batch([ let fonts = Task::batch([
Task::done(Message::PollWebFonts), Task::done(Message::PollWebFonts),
Task::done(Message::ApplyWebFont(primary_font)), Task::done(Message::ApplyWebFont(primary_font)),
]); ]);
// Web can't reach the Patreon API directly (CORS); fetch the CI-built // Web can't reach the Patreon API directly (CORS); fetch the CI-built
// supporters.json served on the same origin instead. // supporters.json served on the same origin instead.
let patrons = Task::perform( let patrons = Task::perform(crate::patreon::fetch_patrons_web(), Message::PatronsFetched);
crate::patreon::fetch_patrons_web(),
Message::PatronsFetched,
);
s.videos_loading = true; s.videos_loading = true;
let videos = Task::perform( let videos = Task::perform(crate::videos::fetch_playlist_web(), Message::VideosFetched);
crate::videos::fetch_playlist_web(),
Message::VideosFetched,
);
s.discussions_loading = true; s.discussions_loading = true;
let discussions = Task::perform( let discussions = Task::perform(
crate::discussions::fetch_discussions_web(), crate::discussions::fetch_discussions_web(),
@@ -3977,9 +3983,14 @@ pub fn run() -> iced::Result {
if let Some(tab) = state.tabs.get(state.active_tab) { if let Some(tab) = state.tabs.get(state.active_tab) {
let dot = if tab.dirty { "● " } else { "" }; let dot = if tab.dirty { "● " } else { "" };
let name = tab.tab_display_name(); let name = tab.tab_display_name();
format!("{}OCS-{} - {}", dot, env!("OCS_BUILD_STAMP"), name) format!(
"{}Open CAD Studio {} - {}",
dot,
env!("OCS_APP_VERSION"),
name
)
} else { } else {
concat!("OCS-", env!("OCS_BUILD_STAMP")).to_string() concat!("Open CAD Studio ", env!("OCS_APP_VERSION")).to_string()
} }
}) })
.theme(|state: &OpenCADStudio, _| state.active_theme.clone()) .theme(|state: &OpenCADStudio, _| state.active_theme.clone())
@@ -4011,7 +4022,9 @@ pub fn run_web() -> iced::Result {
OpenCADStudio::view_main, OpenCADStudio::view_main,
) )
.subscription(OpenCADStudio::subscription) .subscription(OpenCADStudio::subscription)
.title(|_state: &OpenCADStudio| concat!("OCS-", env!("OCS_BUILD_STAMP")).to_string()) .title(|_state: &OpenCADStudio| {
concat!("Open CAD Studio ", env!("OCS_APP_VERSION")).to_string()
})
.theme(|state: &OpenCADStudio| state.active_theme.clone()) .theme(|state: &OpenCADStudio| state.active_theme.clone())
.backend(iced::Backend::Hardware(iced::backend::Api::OpenGL)) .backend(iced::Backend::Hardware(iced::backend::Api::OpenGL))
.font(iced_aw::ICED_AW_FONT_BYTES) .font(iced_aw::ICED_AW_FONT_BYTES)
+1641 -162
View File
File diff suppressed because it is too large. Load diff
+121 -62
View File
@@ -440,15 +440,15 @@ impl OpenCADStudio {
let display_entity = dispatch::entity_in_working_plane(contextual.as_ref(), plane); let display_entity = dispatch::entity_in_working_plane(contextual.as_ref(), plane);
let entity = &display_entity; let entity = &display_entity;
let group_names = self.tabs[i].scene.group_names_for_entity(handle); let group_names = self.tabs[i].scene.group_names_for_entity(handle);
let specialized_primitive = let compact_solid =
crate::scene::model::solid_history::has_specialized_primitive_properties( crate::scene::model::solid_history::has_compact_solid_properties(
&self.tabs[i].scene.document, &self.tabs[i].scene.document,
handle, handle,
); );
let mut sections = let mut sections =
dispatch::properties_sectioned(handle, entity, &text_style_names); dispatch::properties_sectioned(handle, entity, &text_style_names);
if specialized_primitive { if compact_solid {
retain_specialized_primitive_sections(&mut sections); retain_compact_solid_sections(&mut sections);
} }
sections.extend( sections.extend(
crate::scene::model::solid_history::primitive_properties( crate::scene::model::solid_history::primitive_properties(
@@ -636,7 +636,7 @@ impl OpenCADStudio {
} }
} }
if !specialized_primitive && matches!( if matches!(
entity, entity,
acadrust::EntityType::Solid3D(_) acadrust::EntityType::Solid3D(_)
| acadrust::EntityType::Region(_) | acadrust::EntityType::Region(_)
@@ -654,38 +654,44 @@ impl OpenCADStudio {
{ {
use crate::entities::common::ro_prop; use crate::entities::common::ro_prop;
let metrics = mesh.metrics; let metrics = mesh.metrics;
let mut props = vec![ let triple = |values: [f64; 3]| {
ro_prop( format!("{:.6}, {:.6}, {:.6}", values[0], values[1], values[2])
t!("Vertices").as_ref(), };
"mesh_vertices", let mut props = if compact_solid {
metrics.vertices.to_string(), vec![ro_prop(
),
ro_prop(
t!("Triangles").as_ref(),
"mesh_triangles",
metrics.triangles.to_string(),
),
ro_prop(
t!("Surface Area").as_ref(),
"mesh_surface_area",
format!("{:.6}", metrics.surface_area),
),
ro_prop(
t!("Centroid").as_ref(), t!("Centroid").as_ref(),
"mesh_centroid", "mesh_centroid",
format!( triple(metrics.centroid),
"{:.6}, {:.6}, {:.6}", )]
metrics.centroid[0], } else {
metrics.centroid[1], vec![
metrics.centroid[2] ro_prop(
t!("Vertices").as_ref(),
"mesh_vertices",
metrics.vertices.to_string(),
), ),
), ro_prop(
ro_prop( t!("Triangles").as_ref(),
t!("Tessellation").as_ref(), "mesh_triangles",
"mesh_complete", metrics.triangles.to_string(),
if mesh.complete { "Complete" } else { "Partial" }, ),
), ro_prop(
]; t!("Surface Area").as_ref(),
"mesh_surface_area",
format!("{:.6}", metrics.surface_area),
),
ro_prop(
t!("Centroid").as_ref(),
"mesh_centroid",
triple(metrics.centroid),
),
ro_prop(
t!("Tessellation").as_ref(),
"mesh_complete",
if mesh.complete { "Complete" } else { "Partial" },
),
]
};
if mesh.complete if mesh.complete
&& matches!( && matches!(
entity, entity,
@@ -694,14 +700,50 @@ impl OpenCADStudio {
| acadrust::EntityType::Body(_) | acadrust::EntityType::Body(_)
) )
{ {
props.insert( let closed_props = vec![
3, ro_prop(
t!("Moment of inertia").as_ref(),
"mesh_moment_of_inertia",
triple(metrics.moment_of_inertia),
),
ro_prop(
t!("Principal directions").as_ref(),
"mesh_principal_directions",
metrics
.principal_directions
.chunks_exact(3)
.map(|axis| format!("({:.6}, {:.6}, {:.6})", axis[0], axis[1], axis[2]))
.collect::<Vec<_>>()
.join(", "),
),
ro_prop(
t!("Principal moments").as_ref(),
"mesh_principal_moments",
triple(metrics.principal_moments),
),
ro_prop(
t!("Product of inertia").as_ref(),
"mesh_product_of_inertia",
triple(metrics.product_of_inertia),
),
ro_prop(
t!("Radii of gyration").as_ref(),
"mesh_radii_of_gyration",
triple(metrics.radii_of_gyration),
),
ro_prop( ro_prop(
t!("Volume").as_ref(), t!("Volume").as_ref(),
"mesh_volume", "mesh_volume",
format!("{:.6}", metrics.volume), format!("{:.6}", metrics.volume),
), ),
); ];
if compact_solid {
props.extend(closed_props);
} else {
let volume = closed_props.last().cloned().unwrap();
props.insert(3, volume);
props.extend(closed_props.into_iter().take(5));
}
} }
sections.push(crate::scene::model::object::PropSection { sections.push(crate::scene::model::object::PropSection {
title: t!("Mass Properties").into_owned(), title: t!("Mass Properties").into_owned(),
@@ -710,7 +752,7 @@ impl OpenCADStudio {
} }
} }
if !specialized_primitive { if !compact_solid {
sections.extend(crate::entities::object_data::sections( sections.extend(crate::entities::object_data::sections(
&self.tabs[i].scene.document, &self.tabs[i].scene.document,
&self.tabs[i].scene.object_data_cache, &self.tabs[i].scene.object_data_cache,
@@ -2068,8 +2110,8 @@ impl OpenCADStudio {
}); });
} }
} }
if specialized_primitive { if compact_solid {
retain_specialized_primitive_sections(&mut sections); retain_compact_solid_sections(&mut sections);
} }
let title = match entity { let title = match entity {
acadrust::EntityType::Insert(ins) => { acadrust::EntityType::Insert(ins) => {
@@ -2166,7 +2208,7 @@ impl OpenCADStudio {
.collect(); .collect();
let mut sections = aggregate_sections(&local_refs, &text_style_names); let mut sections = aggregate_sections(&local_refs, &text_style_names);
if local_refs.iter().all(|(handle, _)| { if local_refs.iter().all(|(handle, _)| {
crate::scene::model::solid_history::has_specialized_primitive_properties( crate::scene::model::solid_history::has_compact_solid_properties(
&self.tabs[i].scene.document, &self.tabs[i].scene.document,
*handle, *handle,
) )
@@ -2591,7 +2633,7 @@ impl OpenCADStudio {
&mut self, &mut self,
entity: acadrust::EntityType, entity: acadrust::EntityType,
) -> Option<Handle> { ) -> Option<Handle> {
self.commit_entity_handle_with_policies(entity, false, false) self.commit_entity_handle_with_policies(entity, false, false, false)
} }
pub(super) fn commit_entity_handle_with_dimension_policy( pub(super) fn commit_entity_handle_with_dimension_policy(
@@ -2603,6 +2645,7 @@ impl OpenCADStudio {
entity, entity,
preserve_dimension_layer_and_style, preserve_dimension_layer_and_style,
false, false,
false,
) )
} }
@@ -2610,7 +2653,14 @@ impl OpenCADStudio {
&mut self, &mut self,
entity: acadrust::EntityType, entity: acadrust::EntityType,
) -> Option<Handle> { ) -> Option<Handle> {
self.commit_entity_handle_with_policies(entity, false, true) self.commit_entity_handle_with_policies(entity, false, true, false)
}
pub(super) fn commit_entity_handle_preserve_style(
&mut self,
entity: acadrust::EntityType,
) -> Option<Handle> {
self.commit_entity_handle_with_policies(entity, false, true, true)
} }
fn commit_entity_handle_with_policies( fn commit_entity_handle_with_policies(
@@ -2618,6 +2668,7 @@ impl OpenCADStudio {
mut entity: acadrust::EntityType, mut entity: acadrust::EntityType,
preserve_dimension_layer_and_style: bool, preserve_dimension_layer_and_style: bool,
preserve_entity_layer: bool, preserve_entity_layer: bool,
preserve_entity_style: bool,
) -> Option<Handle> { ) -> Option<Handle> {
let i = self.active_tab; let i = self.active_tab;
let tracks_dimension_chain = matches!( let tracks_dimension_chain = matches!(
@@ -2735,25 +2786,32 @@ impl OpenCADStudio {
} }
} }
crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color); if !preserve_entity_style {
crate::scene::view::dispatch::apply_common_prop( crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color);
&mut entity, crate::scene::view::dispatch::apply_common_prop(
"linetype", &mut entity,
&self.ribbon.active_linetype.clone(), "linetype",
); &self.ribbon.active_linetype.clone(),
crate::scene::view::dispatch::apply_line_weight(&mut entity, self.ribbon.active_lineweight); );
// CELTSCALE (header.current_entity_linetype_scale): new entities crate::scene::view::dispatch::apply_line_weight(
// pick up the document's saved per-entity linetype scale. The user &mut entity,
// can override per entity later via the properties panel. self.ribbon.active_lineweight,
let celtscale = self.tabs[i].scene.document.header.current_entity_linetype_scale; );
if (celtscale - 1.0).abs() > 1e-9 && celtscale.abs() > 1e-9 { // CELTSCALE (header.current_entity_linetype_scale): new entities
entity.common_mut().linetype_scale = celtscale; // pick up the document's saved per-entity linetype scale. The user
// can override per entity later via the properties panel.
let celtscale = self.tabs[i].scene.document.header.current_entity_linetype_scale;
if (celtscale - 1.0).abs() > 1e-9 && celtscale.abs() > 1e-9 {
entity.common_mut().linetype_scale = celtscale;
}
} }
crate::scene::creation_style::apply_current_creation_styles( if !preserve_entity_style {
&self.tabs[i].scene.document, crate::scene::creation_style::apply_current_creation_styles(
&mut entity, &self.tabs[i].scene.document,
); &mut entity,
);
}
if let (Some((layer, style_name)), acadrust::EntityType::Dimension(dimension)) = if let (Some((layer, style_name)), acadrust::EntityType::Dimension(dimension)) =
(inherited_dimension, &mut entity) (inherited_dimension, &mut entity)
{ {
@@ -3061,7 +3119,7 @@ fn aggregate_solid_history_sections(
merged merged
} }
fn retain_specialized_primitive_sections( fn retain_compact_solid_sections(
sections: &mut Vec<crate::scene::model::object::PropSection>, sections: &mut Vec<crate::scene::model::object::PropSection>,
) { ) {
sections.iter_mut().for_each(|section| { sections.iter_mut().for_each(|section| {
@@ -3077,7 +3135,8 @@ fn retain_specialized_primitive_sections(
| "transparency" | "transparency"
| "hyperlink" | "hyperlink"
| "material" | "material"
) || crate::scene::model::solid_history::is_specialized_property(property.field) ) || property.field.starts_with("mesh_")
|| crate::scene::model::solid_history::is_specialized_property(property.field)
}); });
}); });
sections.retain(|section| !section.props.is_empty()); sections.retain(|section| !section.props.is_empty());
File diff suppressed because it is too large. Load diff
+8
View File
@@ -740,6 +740,14 @@ impl OpenCADStudio {
.as_mut() .as_mut()
.map(|c| c.on_preview_wires(cur)) .map(|c| c.on_preview_wires(cur))
.unwrap_or_default(); .unwrap_or_default();
let preview_hidden = self.tabs[i]
.active_cmd
.as_ref()
.map(|command| command.preview_hidden_handles().to_vec())
.unwrap_or_default();
self.tabs[i]
.scene
.set_command_preview_hidden(&preview_hidden);
self.tabs[i].scene.set_preview_wires(previews); self.tabs[i].scene.set_preview_wires(previews);
} }
} }
+10 -1
View File
@@ -133,6 +133,9 @@ fn plot_dialog_sheet_mm(d: &crate::ui::window::plot::PlotDialogState) -> (f64, f
"A1" => Some(PaperSize::A1), "A1" => Some(PaperSize::A1),
"A0" => Some(PaperSize::A0), "A0" => Some(PaperSize::A0),
"A4" => Some(PaperSize::A4), "A4" => Some(PaperSize::A4),
"Letter" => Some(PaperSize::Letter),
"Legal" => Some(PaperSize::Legal),
"Tabloid" => Some(PaperSize::Tabloid),
_ => None, _ => None,
}; };
if let Some(paper) = standard { if let Some(paper) = standard {
@@ -4297,7 +4300,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
.as_deref() .as_deref()
.is_some_and(|name| name.eq_ignore_ascii_case(&style_name)); .is_some_and(|name| name.eq_ignore_ascii_case(&style_name));
let (mut paper, orient) = paper_label_from_dims(ps.paper_width, ps.paper_height); let (mut paper, orient) = paper_label_from_dims(ps.paper_width, ps.paper_height);
if !matches!(paper.as_str(), "A4" | "A3" | "A2" | "A1" | "A0") if !matches!(
paper.as_str(),
"A4" | "A3" | "A2" | "A1" | "A0" | "Letter" | "Legal" | "Tabloid"
)
&& !ps.paper_size.is_empty() && !ps.paper_size.is_empty()
{ {
paper = ps.paper_size.clone(); paper = ps.paper_size.clone();
@@ -4401,6 +4407,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
"A2" => PaperSize::A2, "A2" => PaperSize::A2,
"A1" => PaperSize::A1, "A1" => PaperSize::A1,
"A0" => PaperSize::A0, "A0" => PaperSize::A0,
"Letter" => PaperSize::Letter,
"Legal" => PaperSize::Legal,
"Tabloid" => PaperSize::Tabloid,
_ => PaperSize::A4, _ => PaperSize::A4,
}; };
let orient = if d.orientation == "Portrait" { let orient = if d.orientation == "Portrait" {
+24
View File
@@ -265,6 +265,11 @@ impl OpenCADStudio {
self.control_observe_user_message(&msg); self.control_observe_user_message(&msg);
let perf_started = crate::perf::enabled().then(Instant::now); let perf_started = crate::perf::enabled().then(Instant::now);
let perf_label = perf_message_label(&msg); let perf_label = perf_message_label(&msg);
// Keep the stable tab id because dispatch may switch or close tabs.
let perf_edit_before = perf_started.map(|started| {
let tab = &self.tabs[self.active_tab];
(started, tab.id, tab.scene.geometry_epoch)
});
// A modal dialog must capture the keyboard the same way it already // A modal dialog must capture the keyboard the same way it already
// captures the mouse. Otherwise keystrokes from the global key // captures the mouse. Otherwise keystrokes from the global key
// subscription leak past the modal into the command line and fire as // subscription leak past the modal into the command line and fire as
@@ -324,6 +329,17 @@ impl OpenCADStudio {
self.otrack_active = None; self.otrack_active = None;
self.otrack_kind = None; self.otrack_kind = None;
} }
if let Some((started, tab_id, before)) = perf_edit_before {
if let Some(tab) = self.tabs.iter().find(|tab| tab.id == tab_id) {
if tab.scene.geometry_epoch != before {
tab.scene.record_nav_perf_caused(
crate::scene::NavPerfOp::Edit,
perf_label,
started,
);
}
}
}
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 >= 5.0 { if elapsed_ms >= 5.0 {
@@ -1953,6 +1969,14 @@ impl OpenCADStudio {
self.dispatch_command(&cmd) self.dispatch_command(&cmd)
} }
Message::ScriptLine(line) => {
if line.trim().is_empty() {
self.feed_command(crate::command::StepInput::Enter)
} else {
self.run_command_line(&line)
}
}
Message::ToggleLayers => { Message::ToggleLayers => {
if self.active_modal == Some(super::ModalKind::Layers) { if self.active_modal == Some(super::ModalKind::Layers) {
self.ribbon.deactivate_tool_if("LAYERS"); self.ribbon.deactivate_tool_if("LAYERS");
+8
View File
@@ -2499,6 +2499,14 @@ impl OpenCADStudio {
previews.push(guide); previews.push(guide);
} }
} }
let preview_hidden = self.tabs[i]
.active_cmd
.as_ref()
.map(|command| command.preview_hidden_handles().to_vec())
.unwrap_or_default();
self.tabs[i]
.scene
.set_command_preview_hidden(&preview_hidden);
self.tabs[i].scene.set_preview_wires(previews); self.tabs[i].scene.set_preview_wires(previews);
} else { } else {
self.tabs[i].snap_result = None; self.tabs[i].snap_result = None;
-10
View File
@@ -2393,15 +2393,6 @@ impl OpenCADStudio {
let single_instance = crate::io::single_instance::subscribe().map(Message::OpenExternal); let single_instance = crate::io::single_instance::subscribe().map(Message::OpenExternal);
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
let single_instance = Subscription::none(); let single_instance = Subscription::none();
// A second file opened via Finder while this process is already
// running (#1039) — delivered as a warm Apple Event straight to this
// process, bypassing `src/bin/ocs_launcher.rs` entirely (that only
// runs for the initial cold launch). See `io::macos_open_events`.
#[cfg(target_os = "macos")]
let macos_open_events =
crate::io::macos_open_events::subscribe().map(Message::OpenExternal);
#[cfg(not(target_os = "macos"))]
let macos_open_events = Subscription::none();
// Drain plugin-to-host requests on a timer when any plugin is loaded. // Drain plugin-to-host requests on a timer when any plugin is loaded.
// This lets long-lived plugin sessions (e.g. the Python REPL) mutate the // This lets long-lived plugin sessions (e.g. the Python REPL) mutate the
// host document without requiring a user-generated message. // host document without requiring a user-generated message.
@@ -2577,7 +2568,6 @@ impl OpenCADStudio {
autosave, autosave,
plugin_drain, plugin_drain,
single_instance, single_instance,
macos_open_events,
hatch_pattern_keys, hatch_pattern_keys,
keyboard_events, keyboard_events,
]) ])
+50 -93
View File
@@ -1,77 +1,33 @@
//! macOS-only launcher shim, packaged as the .app's `CFBundleExecutable` //! Receives macOS document-open callbacks before the GUI runtime starts.
//! (`packaging/build_macos_signed.sh`) instead of the real GUI binary
//! (#1039 — "Cannot OPEN WITH in FINDER MAC OS").
//! //!
//! ## Why this exists //! The app bundle uses this helper as its `CFBundleExecutable`. It forwards
//! //! Finder URLs to a running editor or starts the sibling GUI binary, then
//! Finder never delivers an opened file as a command-line argument — a //! stays alive so later opens keep reaching the same AppKit delegate.
//! double-click, "Open With", and even a plain `open -a OpenCADStudio
//! file.dwg` (no `--args`) all resolve it, then hand the running/launching
//! process the path as a `kAEOpenDocuments` Apple Event instead. The real
//! binary only ever reads `std::env::args()` (see `src/cli.rs`), so none of
//! those reached it — Finder just showed "cannot open files in this format".
//!
//! The standard, documented fix is an `NSApplicationDelegate` implementing
//! `application:openURLs:`. That is not available inside the real binary:
//! winit already installs its own delegate to own window/lifecycle events
//! (`platform_impl::macos::app_state::ApplicationDelegate`), `NSApplication`
//! has exactly one delegate slot, and winit's class doesn't implement the
//! open-document method. Several other approaches were tried and measured,
//! not assumed, against real Finder-equivalent opens:
//!
//! * Registering a raw `AEInstallEventHandler` callback (bypassing the
//! delegate protocol) from inside the real binary, at every point
//! tried — before `app::run()`, on `iced::Subscription`'s first poll,
//! even as the literal first statement of `main()` — reliably caught
//! every Apple Event sent to an *already-running, cleanly-launched*
//! process, but never a cold-launch event. `io::macos_open_events` still
//! carries this: it handles a *second* file opened while this process
//! is already running.
//! * A raw `AEInstallEventHandler` in a separate, minimal, non-AppKit
//! process (no winit/iced, just a manual `CFRunLoopRunInMode` pump)
//! still missed the cold-launch event, ruling out "the real binary's
//! heavy dependency graph loading before `main()` wins the race".
//! * A from-scratch `NSApplication` with our own delegate (this file) does
//! catch the cold-launch event reliably. But relaying by replacing this
//! process's image (`exec`, same PID) turned out to have its own
//! failure mode: Launch Services tracks "the running instance of this
//! bundle" by the Application Serial Number it handed out at launch —
//! tied to *this* process, not to the real binary. `exec` doesn't change
//! that ASN, but a later document open sent to it after the `exec`
//! silently never arrived anywhere (measured via a real second "Open
//! With", not assumed) — and a plain `spawn`'d child in place of `exec`
//! didn't help either: `lsappinfo` showed Launch Services still
//! addressing *this* launcher's ASN for the next open, now pointed at a
//! process that had already exited.
//!
//! So this process has to stay alive for the app's session — it is, as far
//! as Launch Services is concerned, "the app". Every open it receives (cold
//! or warm) is relayed to the real GUI binary over the exact channel a
//! second CLI invocation already uses (`io::single_instance`): hand off to
//! an already-running editor if one answers, otherwise launch a fresh one
//! with the files on argv. `NSApplicationActivationPolicyAccessory` keeps it
//! out of the Dock and Cmd-Tab — it owns no window and has nothing to show.
//!
//! Kept intentionally free of every other dependency in this crate (iced,
//! wgpu, cadkernel, …) beyond what relaying needs, so its own startup is as
//! close to instant as possible — plausibly part of why the isolated-process
//! approach above catches the cold-launch event when the full binary didn't.
#[cfg(target_os = "macos")]
use std::path::PathBuf; use std::path::PathBuf;
#[cfg(target_os = "macos")]
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(target_os = "macos")]
use objc2::rc::Retained; use objc2::rc::Retained;
#[cfg(target_os = "macos")]
use objc2::runtime::ProtocolObject; use objc2::runtime::ProtocolObject;
#[cfg(target_os = "macos")]
use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
#[cfg(target_os = "macos")]
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate}; use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate};
#[cfg(target_os = "macos")]
use objc2_foundation::{ use objc2_foundation::{
MainThreadMarker, NSArray, NSNotification, NSObject, NSObjectProtocol, NSURL, MainThreadMarker, NSArray, NSNotification, NSObject, NSObjectProtocol, NSURL,
}; };
#[cfg(target_os = "macos")]
use OpenCADStudio::io::single_instance; use OpenCADStudio::io::single_instance;
/// Name of the real GUI binary inside `Contents/MacOS/`, sibling to this /// Name of the real GUI binary inside `Contents/MacOS/`, sibling to this
/// launcher. Must match the packaging script's bundle assembly step. /// launcher. Must match the packaging script's bundle assembly step.
#[cfg(target_os = "macos")]
const REAL_BINARY_NAME: &str = "OpenCADStudio-App"; const REAL_BINARY_NAME: &str = "OpenCADStudio-App";
/// How long to wait, after `applicationDidFinishLaunching:`, for an /// How long to wait, after `applicationDidFinishLaunching:`, for an
@@ -80,13 +36,20 @@ const REAL_BINARY_NAME: &str = "OpenCADStudio-App";
/// file). `application:openURLs:` arrives as part of the same startup /// file). `application:openURLs:` arrives as part of the same startup
/// sequence when it's coming at all — essentially immediately, not after a /// sequence when it's coming at all — essentially immediately, not after a
/// meaningful delay — so this window is slack, not a user-visible wait. /// meaningful delay — so this window is slack, not a user-visible wait.
#[cfg(target_os = "macos")]
const NO_DOCUMENTS_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(400); const NO_DOCUMENTS_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(400);
/// Set once `application:openURLs:` has handled a launch's documents, so the /// Set once `application:openURLs:` has handled a launch's documents, so the
/// delayed "no documents" fallback in `applicationDidFinishLaunching:` knows /// delayed "no documents" fallback in `applicationDidFinishLaunching:` knows
/// to stay out of the way instead of *also* launching a bare instance. /// to stay out of the way instead of *also* launching a bare instance.
#[cfg(target_os = "macos")]
static DOCS_HANDLED: AtomicBool = AtomicBool::new(false); static DOCS_HANDLED: AtomicBool = AtomicBool::new(false);
/// The first spawned GUI owns the launcher lifetime.
#[cfg(target_os = "macos")]
static GUI_STARTED: AtomicBool = AtomicBool::new(false);
#[cfg(target_os = "macos")]
fn real_binary_path() -> PathBuf { fn real_binary_path() -> PathBuf {
let exe = std::env::current_exe().expect("could not resolve the launcher's own path"); let exe = std::env::current_exe().expect("could not resolve the launcher's own path");
exe.parent() exe.parent()
@@ -94,37 +57,36 @@ fn real_binary_path() -> PathBuf {
.join(REAL_BINARY_NAME) .join(REAL_BINARY_NAME)
} }
/// Deliver `files` to a running editor if one answers on the single-instance /// Forward to a running editor or start the bundled GUI.
/// port, otherwise launch a fresh one with `files` on argv — the same #[cfg(target_os = "macos")]
/// channel a second `OpenCADStudio file.dwg` from a terminal already uses.
/// This process (the launcher) is never itself a candidate to serve that
/// port: `try_connect_existing` only ever connects, never binds, so it can't
/// steal the port out from under the real binary that's either already
/// running or about to claim it on its own.
fn deliver_or_launch(files: &[String]) { fn deliver_or_launch(files: &[String]) {
let paths: Vec<PathBuf> = files.iter().map(PathBuf::from).collect(); let paths: Vec<PathBuf> = files.iter().map(PathBuf::from).collect();
let delivered = single_instance::try_connect_existing() if let Some(stream) = single_instance::try_connect_existing() {
.map(|stream| single_instance::handoff(stream, &paths)) if single_instance::handoff(stream, &paths) {
.unwrap_or(false); reassert_accessory_policy();
if !delivered { return;
if let Err(err) = std::process::Command::new(real_binary_path()).args(files).spawn() { }
eprintln!("OpenCADStudio launcher: failed to launch the real binary: {err}"); }
match std::process::Command::new(real_binary_path())
.args(files)
.spawn()
{
Ok(mut child) if !GUI_STARTED.swap(true, Ordering::SeqCst) => {
std::thread::spawn(move || {
let _ = child.wait();
std::process::exit(0);
});
}
Ok(_) => {}
Err(err) => {
eprintln!("OpenCADStudio launcher: failed to launch the GUI: {err}");
} }
} }
reassert_accessory_policy(); reassert_accessory_policy();
} }
/// AppKit activates this process — promoting it from /// Keep the windowless relay out of the Dock after an open event activates it.
/// NSApplicationActivationPolicyAccessory (no Dock icon, set at startup) to #[cfg(target_os = "macos")]
/// a normal foreground app with its own Dock icon — as a side effect of
/// handling an open-document Apple Event, even though nothing here ever
/// shows a window. Measured via `lsappinfo`, not assumed: the launcher's own
/// entry flips from type="UIElement" to type="Foreground" the moment it
/// handles a *second* Apple Event (a fresh cold launch's own first event
/// doesn't trigger this — the policy is presumably still settling at that
/// point). Set it back immediately after relaying, so a warm "Open With"
/// doesn't leave a second, contentless Dock icon sitting next to the real
/// editor's.
fn reassert_accessory_policy() { fn reassert_accessory_policy() {
if let Some(mtm) = MainThreadMarker::new() { if let Some(mtm) = MainThreadMarker::new() {
NSApplication::sharedApplication(mtm) NSApplication::sharedApplication(mtm)
@@ -132,6 +94,7 @@ fn reassert_accessory_policy() {
} }
} }
#[cfg(target_os = "macos")]
declare_class!( declare_class!(
struct Delegate; struct Delegate;
@@ -167,15 +130,7 @@ declare_class!(
deliver_or_launch(&files); deliver_or_launch(&files);
} }
/// Fires when this already-running process is reactivated with no /// Start or activate the GUI when the bundle is reopened without files.
/// visible windows to show — clicking the Dock icon, or Finder
/// launching the bundle again while Launch Services still considers
/// it running (this launcher's own ASN never dies between opens; see
/// the module doc). Without this, quitting the real GUI window while
/// this relay stays alive left the app in a state where reopening it
/// did nothing: no fresh `applicationDidFinishLaunching:` (that only
/// fires once, at this process's own cold start) and no
/// `application:openURLs:` (no document was named).
#[method(applicationShouldHandleReopen:hasVisibleWindows:)] #[method(applicationShouldHandleReopen:hasVisibleWindows:)]
fn should_handle_reopen(&self, _sender: &NSApplication, has_visible_windows: bool) -> bool { fn should_handle_reopen(&self, _sender: &NSApplication, has_visible_windows: bool) -> bool {
if !has_visible_windows { if !has_visible_windows {
@@ -186,6 +141,7 @@ declare_class!(
} }
); );
#[cfg(target_os = "macos")]
impl Delegate { impl Delegate {
fn new(mtm: MainThreadMarker) -> Retained<Self> { fn new(mtm: MainThreadMarker) -> Retained<Self> {
let this = mtm.alloc(); let this = mtm.alloc();
@@ -193,6 +149,7 @@ impl Delegate {
} }
} }
#[cfg(target_os = "macos")]
fn main() { fn main() {
let mtm = MainThreadMarker::new().expect("the launcher must run on the main thread"); let mtm = MainThreadMarker::new().expect("the launcher must run on the main thread");
let app = NSApplication::sharedApplication(mtm); let app = NSApplication::sharedApplication(mtm);
@@ -203,9 +160,9 @@ fn main() {
let proto: &ProtocolObject<dyn NSApplicationDelegate> = ProtocolObject::from_ref(&*delegate); let proto: &ProtocolObject<dyn NSApplicationDelegate> = ProtocolObject::from_ref(&*delegate);
app.setDelegate(Some(proto)); app.setDelegate(Some(proto));
// Stays running for the app's session — see the module doc for why:
// Launch Services keeps addressing future document-opens to this
// process's Application Serial Number, not to whatever it spawns.
// SAFETY: called once, on the main thread, immediately after `setDelegate`. // SAFETY: called once, on the main thread, immediately after `setDelegate`.
unsafe { app.run() }; unsafe { app.run() };
} }
#[cfg(not(target_os = "macos"))]
fn main() {}
+36 -2
View File
@@ -1195,6 +1195,15 @@ pub struct LoftOptions {
pub align_direction: bool, pub align_direction: bool,
} }
/// One face-selection edit made while collecting a solid shell operation.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ShellFaceAction {
Remove(DVec3),
Add(DVec3),
RemoveAll,
AddAll,
}
impl Default for LoftOptions { impl Default for LoftOptions {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -1403,6 +1412,8 @@ pub enum CmdResult {
Measurement(String), Measurement(String),
/// Print a measurement result and keep the command active. /// Print a measurement result and keep the command active.
ReportMeasurement(String), ReportMeasurement(String),
/// Print an input error and keep the command active.
ReportError(String),
/// Print a measurement result, clear the current selection, and keep the command active. /// Print a measurement result, clear the current selection, and keep the command active.
ReportMeasurementAndDeselect(String), ReportMeasurementAndDeselect(String),
/// Clear the current selection and keep the command active at its updated step. /// Clear the current selection and keep the command active at its updated step.
@@ -1576,16 +1587,39 @@ pub enum CmdResult {
options: LoftOptions, options: LoftOptions,
color: [f32; 4], color: [f32; 4],
}, },
/// Round or bevel the straight edge nearest `pick` on a solid. /// Round or bevel one or more resolved B-rep edges on a solid.
SolidEdgeBlend { SolidEdgeBlend {
handle: Handle, handle: Handle,
pick: DVec3, edges: Vec<cadkernel::brep::EdgeKey>,
base_face: Option<cadkernel::brep::FaceKey>,
value: f64, value: f64,
other_value: f64,
fillet: bool, fillet: bool,
}, },
/// Offset a solid and optionally open selected faces.
SolidShell {
handle: Handle,
actions: Vec<ShellFaceAction>,
distance: f64,
},
SolidSubtract { SolidSubtract {
bases: Vec<Handle>, bases: Vec<Handle>,
cutters: Vec<Handle>, cutters: Vec<Handle>,
convert_meshes: bool,
},
/// Split every selected solid or surface with one arbitrary plane.
/// `keep_point == None` retains both sides; otherwise it selects the side
/// containing that WCS point.
SliceEntities {
targets: Vec<Handle>,
plane: cadkernel::space::Plane,
keep_point: Option<DVec3>,
},
/// Split selected solids or surfaces with one selected analytic sheet.
SliceSurfaceEntities {
targets: Vec<Handle>,
cutter: Box<cadkernel::brep::Body>,
keep_point: Option<DVec3>,
}, },
/// INSERT landed on a block that has AttributeDefinitions. /// INSERT landed on a block that has AttributeDefinitions.
/// The host should look up the attdefs for `block_name` from the document /// The host should look up the attdefs for `block_name` from the document
+145 -3
View File
@@ -10,7 +10,39 @@ use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, R
use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::object::{GripApply, GripDef, PropSection};
use crate::scene::model::wire_model::TangentGeom; use crate::scene::model::wire_model::TangentGeom;
fn to_render(circle: &Circle) -> RenderEntity { /// The number of segments to tessellate a circle into, based on its radius and the
/// current viewport's world units per pixel (`wpp`).
///
/// Targets a maximum chord sagitta of ~0.25 pixels on screen so circles stay visually
/// round at any zoom level, while bounded between a minimum of 48 (preventing
/// coarse polygons when small or far away) and a maximum of 4096 (capping buffer size
/// on extreme zoom).
pub fn circle_segments(radius: f64, wpp: Option<f32>) -> usize {
const MIN_SEGMENTS: usize = 48;
const MAX_SEGMENTS: usize = 4096;
let Some(w) = wpp.filter(|&w| w.is_finite() && w > 0.0) else {
return MIN_SEGMENTS;
};
let r = radius.abs();
if !r.is_finite() || r <= 0.0 {
return MIN_SEGMENTS;
}
// Target chord sagitta of ~0.25 px on screen:
// sag = r * (1 - cos(theta / 2)) <= tolerance
let tolerance = (w as f64) * 0.25;
if r <= tolerance {
return MIN_SEGMENTS;
}
let cos_half = (1.0 - tolerance / r).clamp(-1.0, 1.0);
let step = 2.0 * cos_half.acos();
if step <= 0.0 {
return MAX_SEGMENTS;
}
let n = (std::f64::consts::TAU / step).ceil() as usize;
n.clamp(MIN_SEGMENTS, MAX_SEGMENTS)
}
pub fn to_render_with_wpp(circle: &Circle, wpp: Option<f32>) -> RenderEntity {
let cx = circle.center.x; let cx = circle.center.x;
let cy = circle.center.y; let cy = circle.center.y;
let cz = circle.center.z; let cz = circle.center.z;
@@ -31,10 +63,12 @@ fn to_render(circle: &Circle) -> RenderEntity {
radius: r, radius: r,
}; };
let n = circle_segments(r, wpp);
if circle.thickness.abs() > 1e-10 { if circle.thickness.abs() > 1e-10 {
let t = circle.thickness; let t = circle.thickness;
let (nx, ny, nz) = normal; let (nx, ny, nz) = normal;
let n = 64usize; let n = n.max(64);
let tau = std::f64::consts::TAU; let tau = std::f64::consts::TAU;
let circ_pt = |a: f64| -> (f64, f64, f64) { let circ_pt = |a: f64| -> (f64, f64, f64) {
let (c, s) = (a.cos(), a.sin()); let (c, s) = (a.cos(), a.sin());
@@ -76,7 +110,8 @@ fn to_render(circle: &Circle) -> RenderEntity {
} }
// Keep tessellation on the entity curve for large-coordinate precision. // Keep tessellation on the entity curve for large-coordinate precision.
let pts = crate::entities::curve::curve_points(&curve); let max_angle = std::f64::consts::TAU / (n as f64);
let pts = curve.tessellate_angle(max_angle);
RenderEntity { RenderEntity {
pick_tris: Vec::new(), pick_tris: Vec::new(),
@@ -88,6 +123,22 @@ fn to_render(circle: &Circle) -> RenderEntity {
} }
} }
fn to_render(circle: &Circle) -> RenderEntity {
to_render_with_wpp(circle, None)
}
/// Zoom-aware render converter for EntityType::Circle.
pub fn relative_render(
entity: &acadrust::EntityType,
_document: &acadrust::CadDocument,
wpp: Option<f32>,
) -> Option<RenderEntity> {
let acadrust::EntityType::Circle(circle) = entity else {
return None;
};
Some(to_render_with_wpp(circle, wpp))
}
fn grips(circle: &Circle) -> Vec<GripDef> { fn grips(circle: &Circle) -> Vec<GripDef> {
let center = circle.center_wcs(); let center = circle.center_wcs();
let (axis_x, axis_y) = circle.axes_wcs(); let (axis_x, axis_y) = circle.axes_wcs();
@@ -220,3 +271,94 @@ impl crate::entities::traits::MassPropsCalc for Circle {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circle_segments_scales_with_zoom() {
let r = 10.0;
// Default / invalid cases return baseline floor of 48.
assert_eq!(circle_segments(r, None), 48);
assert_eq!(circle_segments(r, Some(-1.0)), 48);
assert_eq!(circle_segments(r, Some(0.0)), 48);
assert_eq!(circle_segments(r, Some(f32::NAN)), 48);
assert_eq!(circle_segments(r, Some(f32::INFINITY)), 48);
// Zoomed far out: screen radius is tiny, capped at floor 48.
assert_eq!(circle_segments(r, Some(10.0)), 48);
assert_eq!(circle_segments(r, Some(2.0)), 48);
// Zooming closer increases segments monotonically.
let seg_0_1 = circle_segments(r, Some(0.1));
let seg_0_01 = circle_segments(r, Some(0.01));
let seg_0_001 = circle_segments(r, Some(0.001));
let seg_0_0001 = circle_segments(r, Some(0.0001));
assert!(seg_0_1 >= 48, "seg_0_1 was {seg_0_1}");
assert!(seg_0_01 > seg_0_1, "seg_0_01 ({seg_0_01}) <= seg_0_1 ({seg_0_1})");
assert!(seg_0_001 > seg_0_01, "seg_0_001 ({seg_0_001}) <= seg_0_01 ({seg_0_01})");
assert!(seg_0_0001 > seg_0_001, "seg_0_0001 ({seg_0_0001}) <= seg_0_001 ({seg_0_001})");
// Extreme zoom in hits maximum cap of 4096.
assert_eq!(circle_segments(r, Some(1e-9)), 4096);
}
#[test]
fn test_circle_segments_scales_with_radius() {
let wpp = Some(0.1);
let seg_small = circle_segments(1.0, wpp);
let seg_med = circle_segments(20.0, wpp);
let seg_large = circle_segments(100.0, wpp);
let seg_huge = circle_segments(500.0, wpp);
assert_eq!(seg_small, 48);
assert!(seg_med > seg_small, "{seg_med} <= {seg_small}");
assert!(seg_large > seg_med, "{seg_large} <= {seg_med}");
assert!(seg_huge > seg_large, "{seg_huge} <= {seg_large}");
}
#[test]
fn test_circle_to_render_points_increase_with_zoom() {
let mut circle = Circle::default();
circle.radius = 10.0;
let far_render = to_render_with_wpp(&circle, Some(1.0));
let close_render = to_render_with_wpp(&circle, Some(0.01));
let far_count = match far_render.object {
RenderObject::Lines(pts) => pts.len(),
_ => panic!("Expected RenderObject::Lines"),
};
let close_count = match close_render.object {
RenderObject::Lines(pts) => pts.len(),
_ => panic!("Expected RenderObject::Lines"),
};
assert_eq!(far_count, 49); // 48 segments + 1 closing vertex
assert!(close_count > far_count, "close_count was {close_count} vs far_count {far_count}");
}
#[test]
fn test_thick_circle_to_render_points_increase_with_zoom() {
let mut circle = Circle::default();
circle.radius = 10.0;
circle.thickness = 2.0;
let far_render = to_render_with_wpp(&circle, Some(1.0));
let close_render = to_render_with_wpp(&circle, Some(0.01));
let far_count = match far_render.object {
RenderObject::Lines(pts) => pts.len(),
_ => panic!("Expected RenderObject::Lines"),
};
let close_count = match close_render.object {
RenderObject::Lines(pts) => pts.len(),
_ => panic!("Expected RenderObject::Lines"),
};
assert!(close_count > far_count, "{close_count} <= {far_count}");
}
}
+38 -3
View File
@@ -114,7 +114,11 @@ pub fn format_length(value: f64) -> String {
let per_foot = 12.0 * scale; let per_foot = 12.0 * scale;
let feet = (total / per_foot).trunc(); let feet = (total / per_foot).trunc();
let rem = (total - feet * per_foot) / scale; let rem = (total - feet * per_foot) / scale;
format!("{}{:.0}'-{:.*}\"", sign, feet, prec, rem) if feet == 0.0 {
format!("{}{:.*}\"", sign, prec, rem)
} else {
format!("{}{:.0}'-{:.*}\"", sign, feet, prec, rem)
}
} }
4 | 5 => { 4 | 5 => {
// Architectural and fractional formats use 1/64-inch resolution. // Architectural and fractional formats use 1/64-inch resolution.
@@ -142,6 +146,9 @@ pub fn format_length(value: f64) -> String {
}; };
let unit_suffix = if ctx.lunits == 4 { "\"" } else { "" }; let unit_suffix = if ctx.lunits == 4 { "\"" } else { "" };
match feet { match feet {
Some(f) if f == 0.0 => {
format!("{}{:.0}{}{}", sign, whole, frac_str, unit_suffix)
}
Some(f) => format!("{}{:.0}'-{:.0}{}{}", sign, f, whole, frac_str, unit_suffix), Some(f) => format!("{}{:.0}'-{:.0}{}{}", sign, f, whole, frac_str, unit_suffix),
None => format!("{}{:.0}{}", sign, whole, frac_str), None => format!("{}{:.0}{}", sign, whole, frac_str),
} }
@@ -702,6 +709,30 @@ pub fn parse_angle_deg(value: &str) -> Option<f64> {
/// there is one obvious place to see that the maths moved out. /// there is one obvious place to see that the maths moved out.
pub use cadkernel::geom2d::BulgeArc; pub use cadkernel::geom2d::BulgeArc;
/// Convert a 2D BulgeArc into a 3D TangentGeom::Arc with its world center,
/// plane axes, radius, and counter-clockwise start/end sweep angles.
pub fn bulge_arc_to_tangent(
arc: &BulgeArc,
to_wcs: &dyn Fn(f64, f64) -> (f64, f64, f64),
normal: (f64, f64, f64),
) -> crate::scene::model::wire_model::TangentGeom {
let (cwx, cwy, cwz) = to_wcs(arc.center[0], arc.center[1]);
let (ax, ay) = crate::scene::view::transform::ocs_axes(normal);
let (sa, ea) = if arc.sweep >= 0.0 {
(arc.start_angle, arc.end_angle)
} else {
(arc.end_angle, arc.start_angle)
};
crate::scene::model::wire_model::TangentGeom::Arc {
center: [cwx, cwy, cwz],
axis_x: [ax.0, ax.1, ax.2],
axis_y: [ay.0, ay.1, ay.2],
radius: arc.radius,
start_angle: sa,
end_angle: ea,
}
}
/// Triangulate the solid bands a `wide_fills` returns into the flat WCS f64 /// Triangulate the solid bands a `wide_fills` returns into the flat WCS f64
/// triangle list `RenderEntity::pick_tris` carries, so a wide polyline is /// triangle list `RenderEntity::pick_tris` carries, so a wide polyline is
/// selectable across the band it draws and not just along its centreline. /// selectable across the band it draws and not just along its centreline.
@@ -1078,7 +1109,9 @@ mod length_format_tests {
#[test] #[test]
fn architectural_carries_into_feet() { fn architectural_carries_into_feet() {
assert_eq!(with_units(4, 4, 11.99), "0'-11 63/64\""); assert_eq!(with_units(4, 4, 11.99), "11 63/64\"");
assert_eq!(with_units(4, 4, 0.5), "0 1/2\"");
assert_eq!(with_units(4, 4, -9.25), "-9 1/4\"");
assert_eq!(with_units(4, 4, 11.999), "1'-0\""); assert_eq!(with_units(4, 4, 11.999), "1'-0\"");
assert_eq!(with_units(4, 4, 23.999), "2'-0\""); assert_eq!(with_units(4, 4, 23.999), "2'-0\"");
assert_eq!(with_units(4, 4, 66.5), "5'-6 1/2\""); assert_eq!(with_units(4, 4, 66.5), "5'-6 1/2\"");
@@ -1087,7 +1120,9 @@ mod length_format_tests {
#[test] #[test]
fn engineering_carries_into_feet() { fn engineering_carries_into_feet() {
assert_eq!(with_units(3, 1, 11.94), "0'-11.9\""); assert_eq!(with_units(3, 1, 11.94), "11.9\"");
assert_eq!(with_units(3, 1, 0.5), "0.5\"");
assert_eq!(with_units(3, 1, -9.25), "-9.3\"");
assert_eq!(with_units(3, 1, 11.99), "1'-0.0\""); assert_eq!(with_units(3, 1, 11.99), "1'-0.0\"");
assert_eq!(with_units(3, 1, 23.99), "2'-0.0\""); assert_eq!(with_units(3, 1, 23.99), "2'-0.0\"");
assert_eq!(with_units(3, 2, 11.999), "1'-0.00\""); assert_eq!(with_units(3, 2, 11.999), "1'-0.00\"");
+7 -7
View File
@@ -294,16 +294,16 @@ fn apply_axis_grip(ell: &mut Ellipse, grip_id: usize, point: glam::DVec3) {
fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) { fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) {
match (grip_id, apply) { match (grip_id, apply) {
(0, GripApply::Translate(d)) => { (0, GripApply::Translate(d)) => {
ell.center.x += d.x as f64; ell.center.x += d.x;
ell.center.y += d.y as f64; ell.center.y += d.y;
ell.center.z += d.z as f64; ell.center.z += d.z;
} }
(0, GripApply::Absolute(p)) => { (0, GripApply::Absolute(p)) => {
ell.center.x = p.x as f64; ell.center.x = p.x;
ell.center.y = p.y as f64; ell.center.y = p.y;
ell.center.z = p.z as f64; ell.center.z = p.z;
} }
(1 | 2 | 3 | 4, GripApply::Absolute(p)) => apply_axis_grip(ell, grip_id, p), (1..=4, GripApply::Absolute(p)) => apply_axis_grip(ell, grip_id, p),
(5 | 6, GripApply::Absolute(p)) => { (5 | 6, GripApply::Absolute(p)) => {
let Some(parameter) = parameter_at_point(ell, p) else { let Some(parameter) = parameter_at_point(ell, p) else {
return; return;
+5 -20
View File
@@ -351,11 +351,7 @@ fn to_render(pline: &LwPolyline, fill_mode: bool) -> RenderEntity {
} else if let Some(arc) = } else if let Some(arc) =
crate::entities::common::BulgeArc::from_bulge([ox0, oy0], [ox1, oy1], bulge) crate::entities::common::BulgeArc::from_bulge([ox0, oy0], [ox1, oy1], bulge)
{ {
let (wcx, wcy, wcz) = to_wcs(arc.center[0], arc.center[1]); tgs.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
tgs.push(TangentGeom::Circle {
center: [wcx as f32, wcy as f32, wcz as f32],
radius: arc.radius as f32,
});
for s in arc for s in arc
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE) .tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
.into_iter() .into_iter()
@@ -407,11 +403,7 @@ fn to_render(pline: &LwPolyline, fill_mode: bool) -> RenderEntity {
[v1.location.x, v1.location.y], [v1.location.x, v1.location.y],
v0.bulge, v0.bulge,
) { ) {
let (wcx, wcy, wcz) = to_wcs(arc.center[0], arc.center[1]); tgs.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
tgs.push(TangentGeom::Circle {
center: [wcx as f32, wcy as f32, wcz as f32],
radius: arc.radius as f32,
});
} }
} }
let (pts, widths) = crate::entities::common::tapered_band_points( let (pts, widths) = crate::entities::common::tapered_band_points(
@@ -470,11 +462,7 @@ fn to_render(pline: &LwPolyline, fill_mode: bool) -> RenderEntity {
} }
let (wx1, wy1, wz1) = to_wcs(ox1, oy1); let (wx1, wy1, wz1) = to_wcs(ox1, oy1);
kv.push([wx1, wy1, wz1]); kv.push([wx1, wy1, wz1]);
let (wcx, wcy, wcz) = to_wcs(arc.center[0], arc.center[1]); tgs.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
tgs.push(TangentGeom::Circle {
center: [wcx as f32, wcy as f32, wcz as f32],
radius: arc.radius as f32,
});
} }
if i + 1 < seg_count { if i + 1 < seg_count {
pts.push([f64::NAN; 3]); pts.push([f64::NAN; 3]);
@@ -565,11 +553,8 @@ fn centerline_metadata(
[end.location.x, end.location.y], [end.location.x, end.location.y],
start.bulge, start.bulge,
) { ) {
let center = to_wcs(arc.center[0], arc.center[1]); let normal = (pline.normal.x, pline.normal.y, pline.normal.z);
tangents.push(TangentGeom::Circle { tangents.push(crate::entities::common::bulge_arc_to_tangent(&arc, to_wcs, normal));
center: [center.0 as f32, center.1 as f32, center.2 as f32],
radius: arc.radius as f32,
});
} }
if index == 0 { if index == 0 {
key_vertices.push([p0.0, p0.1, p0.2]); key_vertices.push([p0.0, p0.1, p0.2]);
+61
View File
@@ -991,6 +991,67 @@ fn display_mesh(mesh: &Mesh) -> RefinedMesh {
refined refined
} }
/// Convert a closed indexed mesh entity to an exact planar-faced B-rep.
/// Open, non-manifold, non-planar, and degenerate meshes return `None` so a
/// modelling command can leave the source untouched.
pub(crate) fn closed_mesh_body(entity: &acadrust::EntityType) -> Option<cadkernel::brep::Body> {
match entity {
acadrust::EntityType::Mesh(mesh) => {
let base = base_refined_mesh(mesh);
cadkernel::brep::make::faceted_solid(&base.vertices, &base.faces)
}
acadrust::EntityType::PolygonMesh(mesh) => {
if !mesh.is_closed_m() || !mesh.is_closed_n() {
return None;
}
let m = mesh.m_vertex_count.max(0) as usize;
let n = mesh.n_vertex_count.max(0) as usize;
let vertices = mesh
.vertices
.iter()
.take(m.saturating_mul(n))
.map(|vertex| v3(&vertex.location))
.collect::<Vec<_>>();
if m < 2 || n < 2 || vertices.len() != m.saturating_mul(n) {
return None;
}
let mut faces = Vec::with_capacity(m.saturating_mul(n));
for row in 0..m {
for column in 0..n {
faces.push(vec![
row * n + column,
((row + 1) % m) * n + column,
((row + 1) % m) * n + (column + 1) % n,
row * n + (column + 1) % n,
]);
}
}
cadkernel::brep::make::faceted_solid(&vertices, &faces)
}
acadrust::EntityType::PolyfaceMesh(mesh) => {
let vertices = mesh
.vertices
.iter()
.map(|vertex| v3(&vertex.location))
.collect::<Vec<_>>();
let faces = mesh
.faces
.iter()
.filter_map(|face| {
let indices = [face.index1, face.index2, face.index3, face.index4]
.into_iter()
.filter(|index| *index != 0)
.map(|index| (index.unsigned_abs() as usize).checked_sub(1))
.collect::<Option<Vec<_>>>()?;
(indices.len() >= 3).then_some(indices)
})
.collect::<Vec<_>>();
cadkernel::brep::make::faceted_solid(&vertices, &faces)
}
_ => None,
}
}
fn face_triangle_indices( fn face_triangle_indices(
vertices: &[[f64; 3]], vertices: &[[f64; 3]],
faces: &[Vec<usize>], faces: &[Vec<usize>],
+5 -16
View File
@@ -248,7 +248,7 @@ fn tessellate_polyline2d(pl: &Polyline2D, fill_mode: bool) -> RenderEntity {
); );
} }
let (tangent_geoms, key_vertices) = let (tangent_geoms, key_vertices) =
centerline_metadata_2d(verts, pl.is_closed(), &to_wcs); centerline_metadata_2d(verts, pl.is_closed(), &to_wcs, normal);
return RenderEntity { return RenderEntity {
pick_tris: Vec::new(), pick_tris: Vec::new(),
object: RenderObject::BoundaryLines { object: RenderObject::BoundaryLines {
@@ -296,11 +296,7 @@ fn tessellate_polyline2d(pl: &Polyline2D, fill_mode: bool) -> RenderEntity {
} else if let Some(arc) = } else if let Some(arc) =
crate::entities::common::BulgeArc::from_bulge([ox0, oy0], [ox1, oy1], bulge) crate::entities::common::BulgeArc::from_bulge([ox0, oy0], [ox1, oy1], bulge)
{ {
let (wcx, wcy, wcz) = to_wcs(arc.center[0], arc.center[1]); tgs.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
tgs.push(TangentGeom::Circle {
center: [wcx as f32, wcy as f32, wcz as f32],
radius: arc.radius as f32,
});
for s in arc for s in arc
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE) .tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
.into_iter() .into_iter()
@@ -378,11 +374,7 @@ fn tessellate_polyline2d(pl: &Polyline2D, fill_mode: bool) -> RenderEntity {
[v1.location.x, v1.location.y], [v1.location.x, v1.location.y],
bulge, bulge,
) { ) {
let (wcx, wcy, wcz) = to_wcs(arc.center[0], arc.center[1]); tangents.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
tangents.push(TangentGeom::Circle {
center: [wcx as f32, wcy as f32, wcz as f32],
radius: arc.radius as f32,
});
} }
if i == 0 { if i == 0 {
@@ -464,6 +456,7 @@ fn centerline_metadata_2d(
verts: &[acadrust::entities::Vertex2D], verts: &[acadrust::entities::Vertex2D],
closed: bool, closed: bool,
to_wcs: &dyn Fn(f64, f64) -> (f64, f64, f64), to_wcs: &dyn Fn(f64, f64) -> (f64, f64, f64),
normal: (f64, f64, f64),
) -> (Vec<TangentGeom>, Vec<[f64; 3]>) { ) -> (Vec<TangentGeom>, Vec<[f64; 3]>) {
let count = verts.len(); let count = verts.len();
let segment_count = if closed { let segment_count = if closed {
@@ -488,11 +481,7 @@ fn centerline_metadata_2d(
[end.location.x, end.location.y], [end.location.x, end.location.y],
start.bulge, start.bulge,
) { ) {
let center = to_wcs(arc.center[0], arc.center[1]); tangents.push(crate::entities::common::bulge_arc_to_tangent(&arc, to_wcs, normal));
tangents.push(TangentGeom::Circle {
center: [center.0 as f32, center.1 as f32, center.2 as f32],
radius: arc.radius as f32,
});
} }
if index == 0 { if index == 0 {
key_vertices.push([p0.0, p0.1, p0.2]); key_vertices.push([p0.0, p0.1, p0.2]);
-283
View File
@@ -1,283 +0,0 @@
//! macOS: catch a `kAEOpenDocuments` Apple Event sent directly to this
//! already-running process — the *warm* half of #1039 ("Cannot OPEN WITH in
//! FINDER MAC OS").
//!
//! [`src/bin/ocs_launcher.rs`] is the `.app`'s actual `CFBundleExecutable`
//! and stays running for the whole session — Launch Services keeps
//! addressing every open (cold *or* warm) to that one process's Application
//! Serial Number, so it relays each one to this binary over the existing
//! `io::single_instance` port rather than over an Apple Event of its own.
//!
//! That covers every open Finder initiates. What it doesn't cover: a plain
//! `open -a OpenCADStudio-App file.dwg`, or an AppleScript `tell application
//! … to open`, aimed at this binary directly rather than at the launcher —
//! bypassing the launcher (and its relay) entirely. Verified against a real
//! second "Open With" while already running (not assumed): Launch Services
//! can deliver a `kAEOpenDocuments` event straight to an already-running,
//! cleanly-launched instance of this binary, and without a handler here that
//! goes unhandled the same way the original bug described.
//!
//! The standard fix — implement `application:openURLs:` on this app's own
//! `NSApplicationDelegate` — isn't available here: winit already installs
//! its own delegate (`platform_impl::macos::app_state::ApplicationDelegate`)
//! to own window/lifecycle events, `NSApplication` has exactly one delegate
//! slot, and winit's class doesn't implement the open-document method. So
//! this bypasses the delegate protocol and registers directly with the
//! lower-level Apple Event Manager (`AEInstallEventHandler`) instead — the
//! same mechanism AppKit's delegate forwarding is itself built on, and
//! independent of who owns the delegate.
//!
//! Registration timing is counter-intuitive and was pinned down by testing
//! against a real `open -a`, not by reasoning from Apple's docs: registering
//! before anything has touched `NSApplication` (e.g. from `main()`, before
//! `app::run()`) does not stick — winit's own `EventLoop::new()`/`-run`
//! startup sequence re-registers AppKit's own default (no-op) handler for
//! the same event afterwards and silently wins. So [`subscribe`]'s stream
//! registers on its own first poll instead, which iced only reaches once the
//! window/application lifecycle has already finished starting — late enough
//! that nothing overwrites it afterwards. (A *cold*-launch event specifically
//! still isn't caught this way, even registered as the first statement of
//! `main()` — which is exactly why that case needs the separate launcher.)
use std::ffi::{c_void, OsString};
use std::os::raw::c_long;
use std::os::unix::ffi::OsStringExt;
use std::path::PathBuf;
use std::sync::mpsc::{channel, Sender};
use std::sync::{Mutex, OnceLock};
// ── Apple Event Manager FFI ─────────────────────────────────────────────
//
// Hand-rolled, matching the existing macOS FFI in `file_association.rs`
// rather than pulling in an objc2 dependency for a handful of C calls. Types
// and constants below are taken from the real SDK headers (verified against
// `AE.framework/Headers/{AppleEvents,AEDataModel}.h`, not from memory) so the
// field widths match this target's actual C ABI.
type OSErr = i16;
type OSType = u32;
type AEEventClass = OSType;
type AEEventID = OSType;
type AEKeyword = OSType;
type DescType = OSType;
/// `SRefCon` is `void *` on this (LP64) target — only ever passed as `0`
/// here, so its exact pointee type doesn't matter.
type SRefCon = *mut c_void;
/// Carbon's `Size` is `typedef long Size;` — 8 bytes on 64-bit macOS.
type Size = c_long;
type Boolean = u8;
/// Layout must match the real C `struct AEDesc` (`descriptorType` then
/// `dataHandle`) exactly, including the compiler-inserted padding before the
/// 8-byte pointer field — `repr(C)` reproduces that automatically here.
#[repr(C)]
struct AEDesc {
descriptor_type: DescType,
data_handle: *mut c_void,
}
type AppleEvent = AEDesc;
type AEDescList = AEDesc;
const K_CORE_EVENT_CLASS: AEEventClass = 0x6165_7674; // 'aevt'
const K_AE_OPEN_DOCUMENTS: AEEventID = 0x6F64_6F63; // 'odoc'
const KEY_DIRECT_OBJECT: AEKeyword = 0x2D2D_2D2D; // '----'
const TYPE_AE_LIST: DescType = 0x6C69_7374; // 'list'
/// Coercing a list item to this type is what makes `AEGetNthDesc` hand back
/// the file's location — as a percent-encoded `file://` URL in practice
/// (measured against a real Finder-equivalent open), not the plain path the
/// `typeFileURL` comment in the real `AEDataModel.h` claims.
const TYPE_FILE_URL: DescType = 0x6675_726C; // 'furl'
type AEEventHandlerProcPtr = unsafe extern "C" fn(
the_apple_event: *const AppleEvent,
reply: *mut AppleEvent,
handler_refcon: SRefCon,
) -> OSErr;
#[link(name = "CoreServices", kind = "framework")]
extern "C" {
fn AEInstallEventHandler(
the_ae_event_class: AEEventClass,
the_ae_event_id: AEEventID,
handler: AEEventHandlerProcPtr,
handler_refcon: SRefCon,
is_sys_handler: Boolean,
) -> OSErr;
fn AEGetParamDesc(
the_apple_event: *const AppleEvent,
the_ae_keyword: AEKeyword,
desired_type: DescType,
result: *mut AEDesc,
) -> OSErr;
fn AECountItems(the_ae_desc_list: *const AEDescList, the_count: *mut c_long) -> OSErr;
fn AEGetNthDesc(
the_ae_desc_list: *const AEDescList,
index: c_long,
desired_type: DescType,
the_ae_keyword: *mut AEKeyword,
result: *mut AEDesc,
) -> OSErr;
fn AEGetDescDataSize(the_ae_desc: *const AEDesc) -> Size;
fn AEGetDescData(the_ae_desc: *const AEDesc, data_ptr: *mut c_void, maximum_size: Size) -> OSErr;
fn AEDisposeDesc(the_ae_desc: *mut AEDesc) -> OSErr;
}
fn null_desc() -> AEDesc {
AEDesc {
descriptor_type: 0,
data_handle: std::ptr::null_mut(),
}
}
/// The Apple Event Manager callback. Runs synchronously, inside whichever
/// native run-loop pass just received the event — kept short, and every path
/// just gets handed off to [`send_opened_path`] rather than touched further
/// here.
unsafe extern "C" fn handle_open_documents(
the_apple_event: *const AppleEvent,
_reply: *mut AppleEvent,
_handler_refcon: SRefCon,
) -> OSErr {
let mut doc_list = null_desc();
let err =
unsafe { AEGetParamDesc(the_apple_event, KEY_DIRECT_OBJECT, TYPE_AE_LIST, &mut doc_list) };
if err != 0 {
return err;
}
let mut count: c_long = 0;
if unsafe { AECountItems(&doc_list, &mut count) } == 0 {
for index in 1..=count {
let mut item = null_desc();
let mut keyword: AEKeyword = 0;
let got =
unsafe { AEGetNthDesc(&doc_list, index, TYPE_FILE_URL, &mut keyword, &mut item) };
if got == 0 {
let size = unsafe { AEGetDescDataSize(&item) };
if size > 0 {
let mut buf = vec![0u8; size as usize];
let read =
unsafe { AEGetDescData(&item, buf.as_mut_ptr() as *mut c_void, size) };
if read == 0 {
if let Some(path) = path_from_bytes(buf) {
send_opened_path(path);
}
}
}
unsafe { AEDisposeDesc(&mut item) };
}
}
}
unsafe { AEDisposeDesc(&mut doc_list) };
0 // noErr
}
/// `typeFileURL` data is a percent-encoded `file://` URL (measured, not
/// assumed — see the module doc). `file:///abs/path` has an empty host, so
/// the part after the scheme already starts with the path's own leading `/`.
fn path_from_bytes(bytes: Vec<u8>) -> Option<PathBuf> {
let s = std::str::from_utf8(&bytes).ok()?;
let encoded_path = s.strip_prefix("file://")?;
let decoded = percent_decode(encoded_path.as_bytes());
if decoded.is_empty() {
return None;
}
Some(PathBuf::from(OsString::from_vec(decoded)))
}
/// Minimal `%XX` decoder. Decodes byte-for-byte rather than per-codepoint, so
/// a multi-byte UTF-8 sequence in the original filename (each byte percent-
/// encoded separately) round-trips correctly through `OsString::from_vec`
/// even though the decoded bytes aren't validated as UTF-8 here.
fn percent_decode(input: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(input.len());
let mut i = 0;
while i < input.len() {
if input[i] == b'%' && i + 2 < input.len() {
if let (Some(hi), Some(lo)) = (hex_val(input[i + 1]), hex_val(input[i + 2])) {
out.push((hi << 4) | lo);
i += 3;
continue;
}
}
out.push(input[i]);
i += 1;
}
out
}
fn hex_val(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
// ── Apple Event callback → iced Subscription ────────────────────────────
//
// Same shape as `single_instance`'s "blocking OS primitive → iced stream"
// bridge: a channel filled from an external source (there, a TCP accept loop
// on its own thread; here, the AE callback), drained on a dedicated thread so
// the async wrapper in [`subscribe`] never blocks.
static SENDER: OnceLock<Mutex<Sender<PathBuf>>> = OnceLock::new();
fn send_opened_path(path: PathBuf) {
if let Some(tx) = SENDER.get() {
let _ = tx.lock().unwrap_or_else(|e| e.into_inner()).send(path);
}
}
/// Files handed to this already-running process via a warm `kAEOpenDocuments`
/// Apple Event. Feeds the same [`crate::app::Message`] path as
/// `single_instance::subscribe` (`Message::OpenExternal`) — see
/// `app::view::subscription`.
pub fn subscribe() -> iced::Subscription<PathBuf> {
iced::Subscription::run(worker)
}
type PathSender = iced::futures::channel::mpsc::Sender<PathBuf>;
fn worker() -> impl iced::futures::Stream<Item = PathBuf> {
iced::stream::channel(8, drain_opened_paths)
}
/// Registers the Apple Event handler on this stream's first poll (see the
/// module doc for why here, of all places, rather than at process startup),
/// then forwards every path the handler receives for the rest of the
/// process's life.
async fn drain_opened_paths(out: PathSender) {
let (tx, rx) = channel();
if SENDER.set(Mutex::new(tx)).is_err() {
// `Subscription::run` keys a recipe's identity off `worker`'s function
// pointer, so iced runs this at most once — this is just a guard
// against that invariant changing out from under us. Park rather than
// end the stream, so iced does not re-run the recipe.
std::future::pending::<()>().await;
return;
}
unsafe {
AEInstallEventHandler(
K_CORE_EVENT_CLASS,
K_AE_OPEN_DOCUMENTS,
handle_open_documents,
std::ptr::null_mut(),
0,
);
}
std::thread::spawn(move || {
let mut out = out;
for path in rx {
let _ = out.try_send(path);
}
});
std::future::pending::<()>().await;
}
+25 -67
View File
@@ -9,8 +9,6 @@ pub mod edit_lock;
pub mod obj; pub mod obj;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub mod single_instance; pub mod single_instance;
#[cfg(target_os = "macos")]
pub mod macos_open_events;
pub mod pdf_export; pub mod pdf_export;
pub mod plot_style; pub mod plot_style;
pub mod print_to_printer; pub mod print_to_printer;
@@ -2116,33 +2114,8 @@ pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool {
|| !finite_coord(a.radius) || !finite_coord(a.radius)
|| !a.start_angle.is_finite() || !a.start_angle.is_finite()
|| !a.end_angle.is_finite() || !a.end_angle.is_finite()
// Same degenerate-curve guard as Circle.
|| a.radius.abs() < 1.0e-10 || a.radius.abs() < 1.0e-10
|| a.radius.abs() > 1.0e10 || a.radius.abs() > 1.0e10
// Zero-sweep arc (start_angle == end_angle, modulo 2π) collapses
// to a single point in WCS — a three-point circle fit on
// coincident vertices recurses unboundedly in parameter_division.
|| (a.end_angle - a.start_angle).abs() < 1.0e-9
// Near-zero sweep is the same trap with a wider mouth: a tiny but
// non-zero sweep (e.g. 1.6e-6 rad) still places start/mid/end
// within the coincidence tolerance, so sampling
// recurses and allocates until OOM. Gate on arc *length*
// (radius × sweep), not sweep alone, so a legitimately large-
// radius small-sweep arc (still a visible curve) survives while
// sub-precision arcs are dropped.
|| a.radius.abs() * (a.end_angle - a.start_angle).abs() < 1.0e-6
// Near-collinear sample points: even when the arc *length* clears
// the floor above, a small sweep over a modest radius leaves
// start/mid/end almost on one line (a 35-unit, 6.5e-7-rad arc has
// arc length 2.3e-5 — past the gate — yet bows off its chord by
// only ~2e-12). A three-point circle fit then returns a
// near-infinite radius and `parameter_division` subdivides without
// bound. Gate on the sagitta (chord height = r·(1−cos(sweep/2))),
// the true measure of how far the arc departs a straight line and
// of the fit's conditioning.
|| a.radius.abs()
* (1.0 - ((a.end_angle - a.start_angle).abs() * 0.5).cos())
< 1.0e-6
|| !finite_unit_normal(&a.normal) || !finite_unit_normal(&a.normal)
} }
E::Ellipse(e) => { E::Ellipse(e) => {
@@ -2172,32 +2145,15 @@ pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool {
let n = s.control_points.len(); let n = s.control_points.len();
let degree_bad = s.degree < 1; let degree_bad = s.degree < 1;
let deg = s.degree.max(0) as usize; let deg = s.degree.max(0) as usize;
let compact_periodic_knots = s.flags.periodic && s.knots.len() == n.saturating_add(1);
let knots_bad = !s.knots.is_empty() let knots_bad = !s.knots.is_empty()
&& (s.knots.iter().any(|k| !k.is_finite()) && (s.knots.iter().any(|k| !k.is_finite())
|| s.knots.windows(2).any(|w| w[1] < w[0]) || s.knots.windows(2).any(|w| w[1] < w[0])
|| s.knots.len() != n + deg + 1); || (!compact_periodic_knots && s.knots.len() != n + deg + 1));
// Degenerate: every control point collapses onto (nearly) the same
// point, so the curve has zero length. A three-point fit
// `parameter_division` never converges on it and the tessellation
// hangs — a periodic 9-point spline pinned at the origin is the seen
// case. Reject when the control-point extent is sub-precision.
let degenerate_extent = n >= 2 && {
let (mut mn, mut mx) = ([f64::MAX; 3], [f64::MIN; 3]);
for p in &s.control_points {
mn[0] = mn[0].min(p.x);
mx[0] = mx[0].max(p.x);
mn[1] = mn[1].min(p.y);
mx[1] = mx[1].max(p.y);
mn[2] = mn[2].min(p.z);
mx[2] = mx[2].max(p.z);
}
(mx[0] - mn[0]).max(mx[1] - mn[1]).max(mx[2] - mn[2]) < 1.0e-6
};
n >= MAX_VERTS n >= MAX_VERTS
|| degree_bad || degree_bad
|| s.control_points.iter().any(|p| !finite_vec3(p)) || s.control_points.iter().any(|p| !finite_vec3(p))
|| knots_bad || knots_bad
|| degenerate_extent
} }
_ => false, _ => false,
} }
@@ -2411,38 +2367,29 @@ mod corrupt_guard_tests {
use acadrust::entities::{Arc, EntityType, Spline}; use acadrust::entities::{Arc, EntityType, Spline};
use acadrust::types::Vector3; use acadrust::types::Vector3;
// A near-zero-sweep arc: sweep 1.56e-6 rad on a 3.9e-3 radius. The angles // Small but finite arcs are valid records. Kernel tessellation is bounded,
// are individually finite and the radius is in range, so the old // so opening must retain them instead of treating their size as corruption.
// (end-start) < 1e-9 check passed it through — but start/mid/end land
// within the coincidence tolerance and sampling allocates
// until OOM. The arc-length floor must reject it.
#[test] #[test]
fn rejects_near_degenerate_arc() { fn keeps_small_finite_arc() {
let mut a = Arc::new(); let mut a = Arc::new();
a.center = Vector3::new(2880.84, 891.83, 0.0); a.center = Vector3::new(2880.84, 891.83, 0.0);
a.radius = 0.0038974142851181423; a.radius = 0.0038974142851181423;
a.start_angle = 1.0401656235942365; a.start_angle = 1.0401656235942365;
a.end_angle = 1.0401671831670538; a.end_angle = 1.0401671831670538;
a.normal = Vector3::new(0.0, 0.0, 1.0); a.normal = Vector3::new(0.0, 0.0, 1.0);
assert!(is_entity_corrupt(&EntityType::Arc(a))); assert!(!is_entity_corrupt(&EntityType::Arc(a)));
} }
// A 35-unit-radius arc sweeping 6.5e-7 rad has arc length 2.3e-5 — past the // A nearly straight arc still carries valid source geometry.
// arc-length floor — yet its start/mid/end bow off the chord by only ~2e-12,
// so a three-point circle fit blows up and sampling hangs. The
// sagitta floor must reject it where the arc-length floor alone does not.
#[test] #[test]
fn rejects_near_collinear_arc() { fn keeps_nearly_straight_arc() {
let mut a = Arc::new(); let mut a = Arc::new();
a.center = Vector3::new(551435.3071786845, 4051623.7156955916, 0.0); a.center = Vector3::new(551435.3071786845, 4051623.7156955916, 0.0);
a.radius = 35.0; a.radius = 35.0;
a.start_angle = 5.823361856481176; a.start_angle = 5.823361856481176;
a.end_angle = 5.823362506017916; a.end_angle = 5.823362506017916;
a.normal = Vector3::new(0.0, 0.0, 1.0); a.normal = Vector3::new(0.0, 0.0, 1.0);
// Sanity: arc length clears the old gate, proving the sagitta gate is assert!(!is_entity_corrupt(&EntityType::Arc(a)));
// what catches this one.
assert!(a.radius * (a.end_angle - a.start_angle).abs() > 1.0e-6);
assert!(is_entity_corrupt(&EntityType::Arc(a)));
} }
// A large-radius small-sweep arc is still a visible curve and must survive: // A large-radius small-sweep arc is still a visible curve and must survive:
@@ -2466,14 +2413,25 @@ mod corrupt_guard_tests {
assert!(is_entity_corrupt(&EntityType::Spline(s))); assert!(is_entity_corrupt(&EntityType::Spline(s)));
} }
// A periodic spline whose control points all collapse onto (nearly) one // A collapsed spline is still a valid source record. Kernel subdivision
// point has zero length; sampling never converges and the // has a fixed depth bound and can process it safely.
// tessellation hangs. The control-point extent floor must reject it.
#[test] #[test]
fn rejects_degenerate_point_spline() { fn keeps_degenerate_point_spline() {
let pts = vec![Vector3::new(1e-12, -1e-12, 0.0); 9]; let pts = vec![Vector3::new(1e-12, -1e-12, 0.0); 9];
let s = Spline::from_control_points(3, pts); let s = Spline::from_control_points(3, pts);
assert!(is_entity_corrupt(&EntityType::Spline(s))); assert!(!is_entity_corrupt(&EntityType::Spline(s)));
}
#[test]
fn keeps_compact_periodic_spline_knots() {
let pts = (0..12)
.map(|index| Vector3::new(index as f64, (index % 3) as f64, 0.0))
.collect();
let mut s = Spline::from_control_points(2, pts);
s.flags.closed = true;
s.flags.periodic = true;
s.knots = (0..13).map(|value| value as f64).collect();
assert!(!is_entity_corrupt(&EntityType::Spline(s)));
} }
// A normal cubic spline (4 control points, valid clamped knots) survives. // A normal cubic spline (4 control points, valid clamped knots) survives.
+19 -4
View File
@@ -1,4 +1,4 @@
//! ISO paper sizes and sheet orientation for window plotting. //! Standard paper sizes and sheet orientation for window plotting.
#[derive(Copy, Clone, PartialEq, Eq, Debug)] #[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum PaperSize { pub enum PaperSize {
@@ -7,15 +7,21 @@ pub enum PaperSize {
A2, A2,
A1, A1,
A0, A0,
Letter,
Legal,
Tabloid,
} }
impl PaperSize { impl PaperSize {
pub const ALL: [PaperSize; 5] = [ pub const ALL: [PaperSize; 8] = [
PaperSize::A4, PaperSize::A4,
PaperSize::A3, PaperSize::A3,
PaperSize::A2, PaperSize::A2,
PaperSize::A1, PaperSize::A1,
PaperSize::A0, PaperSize::A0,
PaperSize::Letter,
PaperSize::Legal,
PaperSize::Tabloid,
]; ];
pub fn label(self) -> &'static str { pub fn label(self) -> &'static str {
@@ -25,6 +31,9 @@ impl PaperSize {
PaperSize::A2 => "A2", PaperSize::A2 => "A2",
PaperSize::A1 => "A1", PaperSize::A1 => "A1",
PaperSize::A0 => "A0", PaperSize::A0 => "A0",
PaperSize::Letter => "Letter",
PaperSize::Legal => "Legal",
PaperSize::Tabloid => "Tabloid",
} }
} }
@@ -36,6 +45,9 @@ impl PaperSize {
PaperSize::A2 => (420.0, 594.0), PaperSize::A2 => (420.0, 594.0),
PaperSize::A1 => (594.0, 841.0), PaperSize::A1 => (594.0, 841.0),
PaperSize::A0 => (841.0, 1189.0), PaperSize::A0 => (841.0, 1189.0),
PaperSize::Letter => (215.9, 279.4),
PaperSize::Legal => (215.9, 355.6),
PaperSize::Tabloid => (279.4, 431.8),
} }
} }
} }
@@ -94,10 +106,13 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn iso_dimensions_and_orientation() { fn paper_dimensions_and_orientation() {
assert_eq!(PaperSize::A4.dimensions_mm(), (210.0, 297.0)); assert_eq!(PaperSize::A4.dimensions_mm(), (210.0, 297.0));
assert_eq!(PaperSize::A0.dimensions_mm(), (841.0, 1189.0)); assert_eq!(PaperSize::A0.dimensions_mm(), (841.0, 1189.0));
assert_eq!(PaperSize::ALL.len(), 5); assert_eq!(PaperSize::Letter.dimensions_mm(), (215.9, 279.4));
assert_eq!(PaperSize::Legal.dimensions_mm(), (215.9, 355.6));
assert_eq!(PaperSize::Tabloid.dimensions_mm(), (279.4, 431.8));
assert_eq!(PaperSize::ALL.len(), 8);
assert_eq!(PaperSize::A3.label(), "A3"); assert_eq!(PaperSize::A3.label(), "A3");
// Portrait keeps (w,h); landscape swaps. // Portrait keeps (w,h); landscape swaps.
assert_eq!(sheet_mm(PaperSize::A4, Orientation::Portrait), (210.0, 297.0)); assert_eq!(sheet_mm(PaperSize::A4, Orientation::Portrait), (210.0, 297.0));
+35 -6
View File
@@ -20,8 +20,6 @@ use printpdf::{
PdfFontHandle, PdfPage, PdfSaveOptions, Point, Polygon, PolygonRing, Pt, Rgb, TextItem, PdfFontHandle, PdfPage, PdfSaveOptions, Point, Polygon, PolygonRing, Pt, Rgb, TextItem,
WindingOrder, WindingOrder,
}; };
#[cfg(not(target_arch = "wasm32"))]
use std::io::Write;
use std::path::Path; use std::path::Path;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -175,8 +173,7 @@ pub fn export_pdf(
plot_style, plot_style,
options, options,
); );
let mut file = std::fs::File::create(path).map_err(|e| e.to_string())?; write_pdf_atomically(path, &bytes)
file.write_all(&bytes).map_err(|e| e.to_string())
} }
/// Export several independently sized pages into one PDF file. /// Export several independently sized pages into one PDF file.
@@ -190,8 +187,22 @@ pub fn export_pdf_pages(
return Err("No pages were selected.".into()); return Err("No pages were selected.".into());
} }
let bytes = build_pdf_pages(pages, plot_style); let bytes = build_pdf_pages(pages, plot_style);
let mut file = std::fs::File::create(path).map_err(|e| e.to_string())?; write_pdf_atomically(path, &bytes)
file.write_all(&bytes).map_err(|e| e.to_string()) }
/// Write a complete PDF beside the destination, then replace it atomically.
#[cfg(not(target_arch = "wasm32"))]
fn write_pdf_atomically(path: &Path, bytes: &[u8]) -> Result<(), String> {
let temp_path = super::save_temp_path(path);
if let Err(error) = std::fs::write(&temp_path, bytes) {
let _ = std::fs::remove_file(&temp_path);
return Err(format!("Failed to write PDF data: {error}"));
}
if let Err(error) = super::replace_save_file(&temp_path, path) {
let _ = std::fs::remove_file(&temp_path);
return Err(format!("Failed to replace PDF file: {error}"));
}
Ok(())
} }
/// Show a parented PDF save-file dialog and return the chosen path. /// Show a parented PDF save-file dialog and return the chosen path.
@@ -1505,6 +1516,24 @@ fn emit_text(
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn atomic_write_replaces_an_existing_pdf() {
let path = std::env::temp_dir().join(format!(
"ocs-pdf-replace-{}-{}.pdf",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::write(&path, b"old").unwrap();
write_pdf_atomically(&path, b"new").unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"new");
std::fs::remove_file(path).unwrap();
}
#[test] #[test]
fn clip_and_scale_emit_pdf_bytes() { fn clip_and_scale_emit_pdf_bytes() {
let w = PlotWire { let w = PlotWire {
+7 -21
View File
@@ -59,16 +59,8 @@ pub enum Claim {
Existing(TcpStream), Existing(TcpStream),
} }
/// The stable path this rendezvous is anchored to: normally the running /// Use the app bundle path on macOS so its launcher and GUI share a port.
/// executable itself, but on macOS, when it lives inside a `.app` bundle's /// Development builds and other platforms remain scoped by executable path.
/// `Contents/MacOS/`, the bundle's own path instead. That makes this
/// launcher shim (`src/bin/ocs_launcher.rs`) and the real `OpenCADStudio-App`
/// binary it relays to — two different executables inside the same bundle —
/// rendezvous on the same port (#1039), rather than each computing a
/// different one from their own distinct `current_exe()`. Falls back to the
/// executable path itself when there's no enclosing bundle (a `cargo run`
/// dev binary, or a non-macOS platform), preserving the anti-collision
/// guarantee below unchanged there.
fn rendezvous_anchor() -> PathBuf { fn rendezvous_anchor() -> PathBuf {
let exe = std::env::current_exe().unwrap_or_default(); let exe = std::env::current_exe().unwrap_or_default();
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -148,13 +140,7 @@ pub fn claim() -> Claim {
} }
} }
/// Try to reach an already-running editor, without attempting to bind the /// Reach an editor without claiming its port. Used by the macOS launcher.
/// port ourselves. Unlike [`claim`], this never competes for the port — for
/// a caller that isn't itself prepared to serve it and go on to become
/// Primary (the macOS launcher shim, `src/bin/ocs_launcher.rs`, which relays
/// to a real editor process rather than being one), calling `claim` would
/// risk binding the port out from under the GUI process that's about to
/// start up and claim it for itself.
pub fn try_connect_existing() -> Option<TcpStream> { pub fn try_connect_existing() -> Option<TcpStream> {
TcpStream::connect_timeout(&addr(), IO_TIMEOUT).ok() TcpStream::connect_timeout(&addr(), IO_TIMEOUT).ok()
} }
@@ -338,15 +324,15 @@ mod tests {
} }
#[test] #[test]
fn rendezvous_key_carries_user_session_and_exe() { fn rendezvous_key_carries_user_session_and_anchor() {
// All three parts must be present, or the isolation the key exists for // All three parts must be present, or the isolation the key exists for
// is silently gone. // is silently gone.
let k = rendezvous_key(); let k = rendezvous_key();
assert_eq!(k.matches('|').count(), 2, "key shape changed: {k:?}"); assert_eq!(k.matches('|').count(), 2, "key shape changed: {k:?}");
let exe = std::env::current_exe().unwrap(); let anchor = rendezvous_anchor();
assert!( assert!(
k.ends_with(&*exe.to_string_lossy()), k.ends_with(&*anchor.to_string_lossy()),
"key must pin the executable: {k:?}" "key must pin the rendezvous anchor: {k:?}"
); );
} }
+9 -8
View File
@@ -1,25 +1,26 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
#![recursion_limit = "256"]
pub mod app; pub mod app;
pub mod config;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub mod cli; pub mod cli;
pub mod command; pub mod command;
pub mod config;
pub mod discussions;
pub mod entities; pub mod entities;
pub mod i18n; pub mod i18n;
pub mod io; pub mod io;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub(crate) mod network;
#[cfg(not(target_arch = "wasm32"))]
pub mod mcp; pub mod mcp;
pub mod modules; pub mod modules;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod network;
pub mod par;
pub mod patreon; pub mod patreon;
pub mod discussions;
pub mod videos;
pub mod plugin;
pub mod perf; pub mod perf;
pub mod plugin;
pub mod scene; pub mod scene;
pub mod snap; pub mod snap;
pub mod ui;
pub mod par;
pub mod sys; pub mod sys;
pub mod ui;
pub mod videos;
+44
View File
@@ -2346,11 +2346,44 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"SLICE offset along X:" => Some(("draw", "slice-offset-along-x")), "SLICE offset along X:" => Some(("draw", "slice-offset-along-x")),
"SLICE offset along Y:" => Some(("draw", "slice-offset-along-y")), "SLICE offset along Y:" => Some(("draw", "slice-offset-along-y")),
"SLICE offset along Z:" => Some(("draw", "slice-offset-along-z")), "SLICE offset along Z:" => Some(("draw", "slice-offset-along-z")),
"SLICE Select solids or surfaces to slice, then press Enter:" => Some(("draw", "slice-select-targets")),
"SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:" => Some(("draw", "slice-first-point-or-method")),
"SLICE Specify first point on slicing plane:" => Some(("draw", "slice-first-plane-point")),
"SLICE Specify second point on slicing plane:" => Some(("draw", "slice-second-plane-point")),
"SLICE Specify third point on slicing plane:" => Some(("draw", "slice-third-plane-point")),
"SLICE Specify a point on the slicing plane:" => Some(("draw", "slice-point-on-plane")),
"SLICE Specify a point on the plane's normal axis:" => Some(("draw", "slice-point-on-normal-axis")),
"SLICE Specify a point on the current view plane:" => Some(("draw", "slice-point-on-view-plane")),
"SLICE Specify a point on the current UCS plane:" => Some(("draw", "slice-point-on-ucs-plane")),
"SLICE Select a planar curve object:" => Some(("draw", "slice-select-planar-object")),
"SLICE Select a planar surface:" => Some(("draw", "slice-select-planar-surface")),
"SLICE Select a cutting surface:" => Some(("draw", "slice-select-cutting-surface")),
"SLICE Specify a point on the desired side or [Both] <Both>:" => Some(("draw", "slice-select-side-or-both")),
"SLICE Select a sliced object to keep or [Both] <Both>:" => Some(("draw", "slice-select-object-or-both")),
"SLICE The selected surface is not planar. Select a planar surface:" => Some(("draw", "slice-surface-not-planar")),
"SLICE The selected surface does not define one supported cutting sheet. Select another surface:" => Some(("draw", "slice-surface-not-supported")),
"SLICE The selected object does not define a plane. Select a planar curve object:" => Some(("draw", "slice-object-not-planar")),
"planar Object" => Some(("draw", "slice-planar-object")),
"Zaxis" => Some(("draw", "slice-zaxis")),
"3points" => Some(("draw", "slice-three-points")),
"Both" => Some(("common", "both")),
"SLICE failed — the plane may not cross the solid." => Some(("common", "slice-failed-the-plane-may-not-cross-the-solid")), "SLICE failed — the plane may not cross the solid." => Some(("common", "slice-failed-the-plane-may-not-cross-the-solid")),
"SLICE: could not determine the solid's extent." => Some(("common", "slice-could-not-determine-the-solid-s-extent")), "SLICE: could not determine the solid's extent." => Some(("common", "slice-could-not-determine-the-solid-s-extent")),
"SLICE: cut at {ax}={value}, kept the {} half." => Some(("common", "slice-cut-at-ax-value-kept-the-half")), "SLICE: cut at {ax}={value}, kept the {} half." => Some(("common", "slice-cut-at-ax-value-kept-the-half")),
"SLICE: select exactly one solid created this session." => Some(("common", "slice-select-exactly-one-solid-created-this-se")), "SLICE: select exactly one solid created this session." => Some(("common", "slice-select-exactly-one-solid-created-this-se")),
"SLICE: the plane does not cross the solid on the kept side." => Some(("common", "slice-the-plane-does-not-cross-the-solid-on-th")), "SLICE: the plane does not cross the solid on the kept side." => Some(("common", "slice-the-plane-does-not-cross-the-solid-on-th")),
"SLICE: select at least one solid or surface." => Some(("common", "slice-select-at-least-one-target")),
"SLICE: the cutting plane is invalid." => Some(("common", "slice-invalid-plane")),
"SLICE: choose a point away from the cutting plane." => Some(("common", "slice-side-point-on-plane")),
"SLICE: the cutting plane does not cross every selected object; no object was changed." => Some(("common", "slice-plane-misses-target")),
"SLICE: the selected geometry has an unsupported surface intersection; no object was changed." => Some(("common", "slice-unsupported-intersection")),
"SLICE: the cutting plane is coincident with source geometry; no object was changed." => Some(("common", "slice-coincident-plane")),
"SLICE: the selected topology could not be split safely; no object was changed." => Some(("common", "slice-topology-refused")),
"SLICE: a result could not be encoded; no object was changed." => Some(("common", "slice-result-encode-failed")),
"SLICE: a result could not be displayed completely; no object was changed." => Some(("common", "slice-result-display-failed")),
"SLICE: a result could not be committed; all source objects were retained." => Some(("common", "slice-result-commit-failed")),
"SLICE: a retained result could not be updated." => Some(("common", "slice-retained-update-failed")),
"SLICE: created %{count} result object(s)." => Some(("common", "slice-created-results")),
"SOLID Specify first point:" => Some(("draw", "solid-specify-first-point")), "SOLID Specify first point:" => Some(("draw", "solid-specify-first-point")),
"SOLID Specify fourth point:" => Some(("draw", "solid-specify-fourth-point")), "SOLID Specify fourth point:" => Some(("draw", "solid-specify-fourth-point")),
"SOLID Specify second point:" => Some(("draw", "solid-specify-second-point")), "SOLID Specify second point:" => Some(("draw", "solid-specify-second-point")),
@@ -3871,8 +3904,19 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"CYLINDER Specify axis endpoint:" => Some(("model", "cylinder-specify-axis-endpoint")), "CYLINDER Specify axis endpoint:" => Some(("model", "cylinder-specify-axis-endpoint")),
"Elliptical" => Some(("model", "elliptical")), "Elliptical" => Some(("model", "elliptical")),
"Select a solid edge:" => Some(("model", "select-a-solid-edge")), "Select a solid edge:" => Some(("model", "select-a-solid-edge")),
"Select an edge or [Chain/Loop/Radius]:" => Some(("model", "filletedge-select-edge-or-chain-loop-radius")),
"Select an edge chain or [Edge/Radius]:" => Some(("model", "filletedge-select-edge-chain-or-edge-radius")),
"Select edge of loop or [Edge/Chain/Radius]:" => Some(("model", "filletedge-select-loop-edge-or-edge-chain-radius")),
"Enter an option [Accept/Next] <Accept>:" => Some(("model", "filletedge-enter-option-accept-next")),
"Press Enter to accept the fillet or [Radius]:" => Some(("model", "filletedge-accept-or-radius")),
"Enter fillet radius or [Expression] <{:.4}>:" => Some(("model", "filletedge-enter-radius-or-expression")),
"Specify Radius or [Expression] <{:.4}>:" => Some(("model", "filletedge-specify-radius-or-expression")),
"Enter expression:" => Some(("model", "filletedge-enter-expression")),
"Specify fillet radius <{:.3}>:" => Some(("model", "specify-fillet-radius")), "Specify fillet radius <{:.3}>:" => Some(("model", "specify-fillet-radius")),
"Specify chamfer distance <{:.3}>:" => Some(("model", "specify-chamfer-distance")), "Specify chamfer distance <{:.3}>:" => Some(("model", "specify-chamfer-distance")),
"A selected edge no longer belongs to the solid." => Some(("model", "filletedge-stale-edge")),
"The solid history could not be created." => Some(("model", "filletedge-history-create-failed")),
"The fillet history could not be recorded." => Some(("model", "filletedge-history-append-failed")),
"POLYSOLID Height = {:.4}, Width = {:.4}, Justification = {}\n{}" => Some(("model", "polysolid-height-width-justification")), "POLYSOLID Height = {:.4}, Width = {:.4}, Justification = {}\n{}" => Some(("model", "polysolid-height-width-justification")),
"Specify start point or [Object/Height/Width/Justify] <Object>:" => Some(("model", "specify-start-point-or-object-height-width-justify-object")), "Specify start point or [Object/Height/Width/Justify] <Object>:" => Some(("model", "specify-start-point-or-object-height-width-justify-object")),
"Specify height <{:.4}>:" => Some(("model", "specify-height")), "Specify height <{:.4}>:" => Some(("model", "specify-height")),
+3 -1
View File
@@ -134,7 +134,9 @@ fn main() -> iced::Result {
Ok(text) => text Ok(text) => text
.lines() .lines()
.map(str::trim) .map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#')) // Blank script rows are significant: they submit Enter to
// the active command (for example, accepting a preview).
.filter(|l| !l.starts_with('#') && !l.starts_with(';'))
.map(str::to_string) .map(str::to_string)
.collect(), .collect(),
Err(e) => { Err(e) => {
+98 -15
View File
@@ -22,11 +22,14 @@ const MODERN_PROTOCOL_VERSION: &str = "2026-07-28";
const MAX_REQUEST: usize = 1_048_576; const MAX_REQUEST: usize = 1_048_576;
const MAX_RESPONSE: u64 = 16 * 1024 * 1024; const MAX_RESPONSE: u64 = 16 * 1024 * 1024;
const CACHE_TTL_MS: u64 = 3_600_000; const CACHE_TTL_MS: u64 = 3_600_000;
const INSTRUCTIONS: &str = "Call ocs_sessions, then ocs_read state before editing. Preserve session_id, document_id, revision, selection and request_id. Use ocs_read commands with parameters.name for a command manifest. Use ocs_execute batch when several steps are known, and request changed_entities when the resulting geometry is needed. For interactive work, call start and follow state.command.accepts, options and input_example. A run.cmd contains the command name followed by prompt answers separated by spaces; points use x,y or x,y,z. After a timeout, query the existing operation and never replay a mutation with a new request_id. waiting_input and running are not completion. Let OCS and its geometry kernel calculate geometry; use query near, contains_point and intersections for exact relationships. Verify important results with queries and a viewport capture, and save only to an explicit path."; const INSTRUCTIONS: &str = "Call ocs_sessions, then pass its session_id as ocs_session_id to ocs_read, ocs_execute and ocs_capture. Read capabilities to discover the complete CAD automation surface. Call record_schema to discover every record type, property path, JSON type, enum, unit, constraint and write rule before editing unfamiliar data. Use records to inspect every serializable entity, object, table, header and document record; filter with RFC 6901 JSON Pointer paths. Use set_properties for atomic, type-checked record edits and preserve document_id, revision and request_id. Use commands with parameters.name for a command manifest. Use batch when several steps are known, and request changed_entities when resulting geometry is needed. For interactive work, call start and follow state.command.accepts, options and input_example. A run.cmd contains the command name followed by prompt answers separated by spaces; points use x,y or x,y,z. After a timeout, query the existing operation and never replay a mutation with a new request_id. waiting_input and running are not completion. Let OCS and its geometry kernel calculate geometry; use query near, contains_point and intersections for exact relationships. Verify important results with queries and a viewport capture, and save only to an explicit path.";
const READ_OPS: &[&str] = &[ const READ_OPS: &[&str] = &[
"state", "state",
"hello", "hello",
"query", "query",
"records",
"record_schema",
"capabilities",
"entities", "entities",
"layers", "layers",
"header", "header",
@@ -38,12 +41,39 @@ const READ_OPS: &[&str] = &[
"operation", "operation",
]; ];
const EXECUTE_OPS: &[&str] = &[ const EXECUTE_OPS: &[&str] = &[
"new", "open", "activate", "run", "start", "input", "cancel", "undo", "redo", "select", "new",
"property", "action", "save", "stop", "batch", "open",
"activate",
"run",
"start",
"input",
"cancel",
"undo",
"redo",
"select",
"property",
"set_properties",
"action",
"save",
"stop",
"batch",
]; ];
const BATCH_STEP_OPS: &[&str] = &[ const BATCH_STEP_OPS: &[&str] = &[
"new", "open", "activate", "run", "start", "input", "cancel", "undo", "redo", "select", "new",
"property", "action", "save", "stop", "open",
"activate",
"run",
"start",
"input",
"cancel",
"undo",
"redo",
"select",
"property",
"set_properties",
"action",
"save",
"stop",
]; ];
const MAX_BATCH_STEPS: usize = 64; const MAX_BATCH_STEPS: usize = 64;
@@ -671,6 +701,15 @@ fn validate_execute_request(request: &Value, op: &str) -> Result<(), String> {
r#"{"op":"property","field":"color","value":1}"#, r#"{"op":"property","field":"color","value":1}"#,
) )
} }
"set_properties"
if request["collection"].as_str().is_none_or(str::is_empty)
|| request["updates"].as_array().is_none_or(Vec::is_empty) =>
{
missing(
"collection or updates",
r#"{"op":"set_properties","collection":"entities","handle":"2A","updates":[{"path":"/common/layer","value":"Walls"}]}"#,
)
}
"action" => match request["name"].as_str() { "action" => match request["name"].as_str() {
Some(name) if crate::app::automation_action_names().contains(&name) => Ok(()), Some(name) if crate::app::automation_action_names().contains(&name) => Ok(()),
Some(name) => Err(format!( Some(name) => Err(format!(
@@ -756,7 +795,7 @@ fn call_tool(
Ok(Value::Array(sessions(launch)?)) Ok(Value::Array(sessions(launch)?))
} }
"ocs_read" => { "ocs_read" => {
let session_id = required_string(arguments, "session_id")?; let session_id = required_string(arguments, "ocs_session_id")?;
let op = arguments["op"].as_str().unwrap_or("state"); let op = arguments["op"].as_str().unwrap_or("state");
if !READ_OPS.contains(&op) { if !READ_OPS.contains(&op) {
return Err("Use ocs_execute for mutations".into()); return Err("Use ocs_execute for mutations".into());
@@ -769,7 +808,7 @@ fn call_tool(
client(clients, session_id)?.request(Value::Object(request), 30.0) client(clients, session_id)?.request(Value::Object(request), 30.0)
} }
"ocs_execute" => { "ocs_execute" => {
let session_id = required_string(arguments, "session_id")?; let session_id = required_string(arguments, "ocs_session_id")?;
let request = arguments["request"] let request = arguments["request"]
.as_object() .as_object()
.cloned() .cloned()
@@ -795,7 +834,7 @@ fn call_tool(
shape_execute_response(response, detail, gui) shape_execute_response(response, detail, gui)
} }
"ocs_capture" => { "ocs_capture" => {
let session_id = required_string(arguments, "session_id")?; let session_id = required_string(arguments, "ocs_session_id")?;
let path = std::env::temp_dir().join(format!("ocs-capture-{}.png", random_id()?)); let path = std::env::temp_dir().join(format!("ocs-capture-{}.png", random_id()?));
let scope = arguments["scope"].as_str().unwrap_or("viewport"); let scope = arguments["scope"].as_str().unwrap_or("viewport");
let max_dimension = arguments["max_dimension"].as_u64().unwrap_or(1600); let max_dimension = arguments["max_dimension"].as_u64().unwrap_or(1600);
@@ -833,6 +872,8 @@ fn batch_step_schema() -> Value {
"clear":{"type":"boolean"}, "clear":{"type":"boolean"},
"field":{"type":"string"}, "field":{"type":"string"},
"value":{"description":"New property value; its JSON type must match the property kind.","anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"object"},{"type":"array"},{"type":"null"}]}, "value":{"description":"New property value; its JSON type must match the property kind.","anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"object"},{"type":"array"},{"type":"null"}]},
"collection":{"type":"string"},
"updates":{"type":"array","minItems":1,"items":{"type":"object","properties":{"path":{"type":"string","pattern":"^/"},"value":{},"expected":{}},"required":["path","value"],"additionalProperties":false}},
"name":{"type":"string","enum":crate::app::automation_action_names()} "name":{"type":"string","enum":crate::app::automation_action_names()}
}, },
"required":["op"], "required":["op"],
@@ -873,6 +914,8 @@ fn execute_request_schema() -> Value {
"clear":{"type":"boolean","description":"Clear the current selection before applying select filters."}, "clear":{"type":"boolean","description":"Clear the current selection before applying select filters."},
"field":{"type":"string","minLength":1,"description":"Property id returned by ocs_read properties."}, "field":{"type":"string","minLength":1,"description":"Property id returned by ocs_read properties."},
"value":{"description":"New property value; its JSON type must match the property kind.","anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"object"},{"type":"array"},{"type":"null"}]}, "value":{"description":"New property value; its JSON type must match the property kind.","anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"object"},{"type":"array"},{"type":"null"}]},
"collection":{"type":"string","description":"Record collection returned by ocs_read records."},
"updates":{"type":"array","minItems":1,"description":"Atomic, type-checked property replacements. Paths are RFC 6901 JSON Pointers relative to record.properties.","items":{"type":"object","properties":{"path":{"type":"string","pattern":"^/"},"value":{},"expected":{"description":"Optional compare-and-set value."}},"required":["path","value"],"additionalProperties":false}},
"name":{"type":"string","enum":crate::app::automation_action_names(),"description":"UI action returned by ocs_read commands."}, "name":{"type":"string","enum":crate::app::automation_action_names(),"description":"UI action returned by ocs_read commands."},
"steps":{"type":"array","minItems":1,"maxItems":MAX_BATCH_STEPS,"description":"Sequential editor operations executed with fresh state and idempotency keys. Execution stops at the first failure; completed_steps says what committed.","items":batch_step_schema()} "steps":{"type":"array","minItems":1,"maxItems":MAX_BATCH_STEPS,"description":"Sequential editor operations executed with fresh state and idempotency keys. Execution stops at the first failure; completed_steps says what committed.","items":batch_step_schema()}
}, },
@@ -898,6 +941,7 @@ fn execute_request_schema() -> Value {
{"properties":{"op":{"const":"redo"}}}, {"properties":{"op":{"const":"redo"}}},
{"properties":{"op":{"const":"select"}}}, {"properties":{"op":{"const":"select"}}},
{"properties":{"op":{"const":"property"}},"required":["field","value"]}, {"properties":{"op":{"const":"property"}},"required":["field","value"]},
{"properties":{"op":{"const":"set_properties"}},"required":["collection","updates"]},
{"properties":{"op":{"const":"action"}},"required":["name"]}, {"properties":{"op":{"const":"action"}},"required":["name"]},
{"properties":{"op":{"const":"save"}}}, {"properties":{"op":{"const":"save"}}},
{"properties":{"op":{"const":"stop"}}}, {"properties":{"op":{"const":"stop"}}},
@@ -943,22 +987,22 @@ fn tool_definitions() -> Value {
}, },
{ {
"name":"ocs_read", "name":"ocs_read",
"description":"Read state, command manifests, entities, layers, properties, kernel measurements and spatial relationships, history, events or operation status from a live OCS session.", "description":"Discover capabilities and record schemas, or read state, complete database records, command manifests, entities, properties, kernel measurements and spatial relationships, history, events or operation status from a live OCS session.",
"inputSchema":{"type":"object","properties":{"session_id":{"type":"string","minLength":1,"description":"Session returned by ocs_sessions."},"op":{"type":"string","enum":READ_OPS,"default":"state"},"parameters":{"type":"object","description":"Operation-specific filters.","properties":{"name":{"type":"string","description":"Command name for detailed commands help."},"search":{"type":"string","description":"Case-insensitive command-name search."},"document_id":{"type":"integer","minimum":0},"handle":{"type":"string"},"handles":{"type":"array","items":{"type":"string"},"description":"Exact entity handles for query or measure."},"type":{"type":"string","description":"Entity type filter for query."},"layer":{"type":"string","description":"Layer name filter for query."},"detail":{"type":"string","enum":["summary","geometry","full"],"default":"geometry","description":"Entity detail returned by query."},"fields":{"type":"array","items":{"type":"string"},"description":"Return only these entity fields plus handle."},"near":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Rank planar curves by exact kernel distance to this world XY point."},"contains_point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Return closed planar curves containing this world XY point."},"bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"Filter entities whose world XY bounds overlap [min_x,min_y,max_x,max_y]."},"intersections":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Return exact kernel intersections between two planar curve handles."},"after":{"type":"integer","minimum":0,"description":"Event cursor."},"request_id":{"type":"string","description":"Operation id to query."},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":10000}},"additionalProperties":false}},"required":["session_id"],"additionalProperties":false}, "inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"op":{"type":"string","enum":READ_OPS,"default":"state"},"parameters":{"type":"object","description":"Operation-specific filters.","properties":{"name":{"type":"string","description":"Command name or record name."},"search":{"type":"string","description":"Case-insensitive command or record-type search."},"document_id":{"type":"integer","minimum":0},"collection":{"type":"string","description":"Record collection, all for records, or omit to discover collections and schema types."},"handle":{"type":"string"},"handles":{"type":"array","items":{"type":"string"},"description":"Exact entity or record handles."},"type":{"type":"string","description":"Entity or record type filter; for record_schema, returns its complete type graph and writable field paths."},"layer":{"type":"string","description":"Layer name filter for query."},"detail":{"type":"string","enum":["summary","geometry","full"],"default":"geometry","description":"Entity detail returned by query."},"fields":{"type":"array","items":{"type":"string"},"description":"Return only these entity fields plus handle."},"paths":{"type":"array","items":{"type":"string"},"description":"Project RFC 6901 JSON Pointer paths relative to record.properties."},"where":{"type":"array","description":"All property filters must match.","items":{"type":"object","properties":{"path":{"type":"string"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","contains","starts_with","ends_with","in","exists","not_exists"],"default":"eq"},"value":{}},"required":["path"],"additionalProperties":false}},"near":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Rank planar curves by exact kernel distance to this world XY point."},"contains_point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Return closed planar curves containing this world XY point."},"bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"Filter entities whose world XY bounds overlap [min_x,min_y,max_x,max_y]."},"intersections":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Return exact kernel intersections between two planar curve handles."},"after":{"type":"integer","minimum":0,"description":"Event cursor."},"request_id":{"type":"string","description":"Operation id to query."},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":10000}},"additionalProperties":false}},"required":["ocs_session_id"],"additionalProperties":false},
"outputSchema":read_output_schema(), "outputSchema":read_output_schema(),
"annotations":{"title":"Read OCS state","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false} "annotations":{"title":"Read OCS state","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}
}, },
{ {
"name":"ocs_execute", "name":"ocs_execute",
"description":"Execute semantic OCS actions. Use current state fields and a unique request_id. Use run for one complete command, batch to remove round trips, or start plus input for guided steps. accepted, running and waiting_input are not completion.", "description":"Execute semantic OCS actions. Use current state fields and a unique request_id. Use run for one complete command, batch to remove round trips, or start plus input for guided steps. accepted, running and waiting_input are not completion.",
"inputSchema":{"type":"object","properties":{"session_id":{"type":"string","minLength":1,"description":"Session returned by ocs_sessions."},"request":execute_request_schema(),"wait_seconds":{"type":"number","minimum":0,"maximum":60,"default":30,"description":"Total time to wait for completion before returning."},"response_detail":{"type":"string","enum":["compact","changed_entities","full"],"default":"compact","description":"compact returns only state needed for the next edit; changed_entities also returns current geometry for changed handles; full preserves the complete editor state."}},"required":["session_id","request"],"additionalProperties":false}, "inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"request":execute_request_schema(),"wait_seconds":{"type":"number","minimum":0,"maximum":60,"default":30,"description":"Total time to wait for completion before returning."},"response_detail":{"type":"string","enum":["compact","changed_entities","full"],"default":"compact","description":"compact returns only state needed for the next edit; changed_entities also returns current geometry for changed handles; full preserves the complete editor state."}},"required":["ocs_session_id","request"],"additionalProperties":false},
"outputSchema":execute_output_schema(), "outputSchema":execute_output_schema(),
"annotations":{"title":"Execute OCS action","readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false} "annotations":{"title":"Execute OCS action","readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false}
}, },
{ {
"name":"ocs_capture", "name":"ocs_capture",
"description":"Capture the actual current OCS drawing viewport or window as a bounded PNG for visual verification.", "description":"Capture the actual current OCS drawing viewport or window as a bounded PNG for visual verification.",
"inputSchema":{"type":"object","properties":{"session_id":{"type":"string","minLength":1,"description":"Session returned by ocs_sessions."},"scope":{"type":"string","enum":["viewport","window"],"default":"viewport","description":"Capture only the drawing viewport by default, or the complete application window."},"max_dimension":{"type":"integer","minimum":256,"maximum":4096,"default":1600,"description":"Resize the longest image edge to at most this many pixels."}},"required":["session_id"],"additionalProperties":false}, "inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"scope":{"type":"string","enum":["viewport","window"],"default":"viewport","description":"Capture only the drawing viewport by default, or the complete application window."},"max_dimension":{"type":"integer","minimum":256,"maximum":4096,"default":1600,"description":"Resize the longest image edge to at most this many pixels."}},"required":["ocs_session_id"],"additionalProperties":false},
"annotations":{"title":"Capture OCS window","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false} "annotations":{"title":"Capture OCS window","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}
} }
]) ])
@@ -1271,6 +1315,19 @@ mod tests {
["ocs_sessions", "ocs_read", "ocs_execute", "ocs_capture"] ["ocs_sessions", "ocs_read", "ocs_execute", "ocs_capture"]
); );
assert_eq!(tools[0]["annotations"]["readOnlyHint"], false); assert_eq!(tools[0]["annotations"]["readOnlyHint"], false);
for tool in [&tools[1], &tools[2], &tools[3]] {
assert!(
tool["inputSchema"]["properties"]
.get("session_id")
.is_none()
);
assert!(
tool["inputSchema"]["required"]
.as_array()
.unwrap()
.contains(&json!("ocs_session_id"))
);
}
assert_eq!( assert_eq!(
tools[2]["inputSchema"]["properties"]["request"]["required"], tools[2]["inputSchema"]["properties"]["request"]["required"],
json!(["op", "request_id"]) json!(["op", "request_id"])
@@ -1298,6 +1355,32 @@ mod tests {
tools[2]["inputSchema"]["properties"]["request"]["properties"]["cmd"]["examples"][0], tools[2]["inputSchema"]["properties"]["request"]["properties"]["cmd"]["examples"][0],
"LINE 0,0 10,10" "LINE 0,0 10,10"
); );
assert!(READ_OPS.contains(&"capabilities"));
assert!(READ_OPS.contains(&"records"));
assert!(READ_OPS.contains(&"record_schema"));
assert!(EXECUTE_OPS.contains(&"set_properties"));
assert_eq!(
tools[1]["inputSchema"]["properties"]["parameters"]["properties"]["where"]["items"]["properties"]
["op"]["enum"],
json!([
"eq",
"ne",
"lt",
"lte",
"gt",
"gte",
"contains",
"starts_with",
"ends_with",
"in",
"exists",
"not_exists"
])
);
assert_eq!(
tools[2]["inputSchema"]["properties"]["request"]["properties"]["updates"]["items"]["required"],
json!(["path", "value"])
);
assert!(tools[0].get("outputSchema").is_some()); assert!(tools[0].get("outputSchema").is_some());
assert!(tools[1].get("outputSchema").is_some()); assert!(tools[1].get("outputSchema").is_some());
assert!(tools[2].get("outputSchema").is_some()); assert!(tools[2].get("outputSchema").is_some());
@@ -1368,7 +1451,7 @@ mod tests {
fn read_tool_rejects_mutations_before_connecting() { fn read_tool_rejects_mutations_before_connecting() {
let mut clients = HashMap::new(); let mut clients = HashMap::new();
let called = handle_message( let called = handle_message(
json!({"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"ocs_read","arguments":{"session_id":"missing","op":"save"}}}), json!({"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"ocs_read","arguments":{"ocs_session_id":"missing","op":"save"}}}),
&mut clients, &mut clients,
&mut TaskStore::default(), &mut TaskStore::default(),
) )
@@ -1397,7 +1480,7 @@ mod tests {
fn execute_requires_a_visible_request_id() { fn execute_requires_a_visible_request_id() {
let mut clients = HashMap::new(); let mut clients = HashMap::new();
let called = handle_message( let called = handle_message(
json!({"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"ocs_execute","arguments":{"session_id":"missing","request":{"op":"undo"}}}}), json!({"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"ocs_execute","arguments":{"ocs_session_id":"missing","request":{"op":"undo"}}}}),
&mut clients, &mut clients,
&mut TaskStore::default(), &mut TaskStore::default(),
) )
@@ -1415,7 +1498,7 @@ mod tests {
fn execute_errors_explain_missing_operation_fields() { fn execute_errors_explain_missing_operation_fields() {
let mut clients = HashMap::new(); let mut clients = HashMap::new();
let called = handle_message( let called = handle_message(
json!({"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"ocs_execute","arguments":{"session_id":"missing","request":{"op":"run","request_id":"run-1"}}}}), json!({"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"ocs_execute","arguments":{"ocs_session_id":"missing","request":{"op":"run","request_id":"run-1"}}}}),
&mut clients, &mut clients,
&mut TaskStore::default(), &mut TaskStore::default(),
) )
+15 -6
View File
@@ -7,7 +7,7 @@ use cadkernel::geom2d::{self, Curve as KernelCurve};
use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::IconKind; use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::{TangentGeom, WireModel};
use glam::DVec3; use glam::DVec3;
const TAU: f64 = std::f64::consts::TAU; const TAU: f64 = std::f64::consts::TAU;
@@ -71,9 +71,9 @@ fn arc_preview(
end_angle: f64, end_angle: f64,
plane: WorkingPlane, plane: WorkingPlane,
) -> Option<WireModel> { ) -> Option<WireModel> {
let center = plane.to_local(center); let center_local = plane.to_local(center);
let arc = geom2d::bounded_arc( let arc = geom2d::bounded_arc(
[center.x, center.y], [center_local.x, center_local.y],
radius, radius,
start_angle, start_angle,
end_angle, end_angle,
@@ -81,14 +81,23 @@ fn arc_preview(
let points = KernelCurve::Arc(arc) let points = KernelCurve::Arc(arc)
.tessellate_angle(TAU / 64.0) .tessellate_angle(TAU / 64.0)
.into_iter() .into_iter()
.map(|point| plane.to_world(DVec3::new(point[0], point[1], center.z)).to_array()) .map(|point| plane.to_world(DVec3::new(point[0], point[1], center_local.z)).to_array())
.collect(); .collect();
Some(WireModel::solid_f64( let mut wire = WireModel::solid_f64(
"rubber_band".into(), "rubber_band".into(),
points, points,
WireModel::CYAN, WireModel::CYAN,
false, false,
)) );
wire.tangent_geoms.push(TangentGeom::Arc {
center: [center.x, center.y, center.z],
axis_x: plane.x.to_array(),
axis_y: plane.y.to_array(),
radius: arc.radius,
start_angle: arc.start_angle,
end_angle: arc.end_angle,
});
Some(wire)
} }
fn make_arc( fn make_arc(
+12 -5
View File
@@ -11,7 +11,7 @@ use crate::t;
use crate::command::{CadCommand, CmdResult, DynField, TangentObject, WorkingPlane}; use crate::command::{CadCommand, CmdResult, DynField, TangentObject, WorkingPlane};
use crate::modules::draw::defaults; use crate::modules::draw::defaults;
use crate::modules::IconKind; use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::{TangentGeom, WireModel};
use glam::DVec3; use glam::DVec3;
const TAU: f64 = std::f64::consts::TAU; const TAU: f64 = std::f64::consts::TAU;
@@ -56,16 +56,23 @@ pub const ICON: IconKind = ICON_CR;
// ── Shared geometry ──────────────────────────────────────────────────────── // ── Shared geometry ────────────────────────────────────────────────────────
fn circle_wire(center: DVec3, radius: f64, plane: WorkingPlane) -> WireModel { fn circle_wire(center: DVec3, radius: f64, plane: WorkingPlane) -> WireModel {
let center = plane.to_local(center); let center_local = plane.to_local(center);
let points = KernelCurve::Circle(KernelCircle { let points = KernelCurve::Circle(KernelCircle {
centre: [center.x, center.y], centre: [center_local.x, center_local.y],
radius, radius,
}) })
.tessellate_angle(TAU / 64.0) .tessellate_angle(TAU / 64.0)
.into_iter() .into_iter()
.map(|point| plane.to_world(DVec3::new(point[0], point[1], center.z)).to_array()) .map(|point| plane.to_world(DVec3::new(point[0], point[1], center_local.z)).to_array())
.collect(); .collect();
WireModel::solid_f64("rubber_band".into(), points, WireModel::CYAN, false) let mut wire = WireModel::solid_f64("rubber_band".into(), points, WireModel::CYAN, false);
wire.tangent_geoms.push(TangentGeom::PlanarCircle {
center: [center.x, center.y, center.z],
axis_x: plane.x.to_array(),
axis_y: plane.y.to_array(),
radius,
});
wire
} }
fn make_circle(center: DVec3, radius: f64, plane: WorkingPlane) -> EntityType { fn make_circle(center: DVec3, radius: f64, plane: WorkingPlane) -> EntityType {
+11 -2
View File
@@ -11,7 +11,7 @@ use crate::t;
use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::IconKind; use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::{TangentGeom, WireModel};
use glam::DVec3; use glam::DVec3;
fn parse_num(text: &str) -> Option<f64> { fn parse_num(text: &str) -> Option<f64> {
@@ -75,7 +75,16 @@ fn ellipse_wire(
[p.x, p.y, p.z] [p.x, p.y, p.z]
}) })
.collect(); .collect();
WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false) let mut wire = WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false);
wire.tangent_geoms.push(TangentGeom::PlanarEllipse {
center: [center.x, center.y, center.z],
major_axis: [major.x, major.y, major.z],
normal: plane.z.to_array(),
minor_axis_ratio: ratio,
start_param: t_start,
end_param: t_e,
});
wire
} }
/// Convert a world point to the parametric angle on the ellipse. /// Convert a world point to the parametric angle on the ellipse.
+62 -5
View File
@@ -17,7 +17,7 @@ use crate::t;
use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::{TangentGeom, WireModel};
// ── Ribbon definition ────────────────────────────────────────────────────── // ── Ribbon definition ──────────────────────────────────────────────────────
@@ -443,6 +443,7 @@ impl CadCommand for PlineCommand {
let pt = self.plane.to_local(pt_world).as_vec3(); let pt = self.plane.to_local(pt_world).as_vec3();
let mut pts: Vec<[f32; 3]> = Vec::new(); let mut pts: Vec<[f32; 3]> = Vec::new();
let mut tangent_geom = None;
match self.mode { match self.mode {
SegMode::Line => { SegMode::Line => {
pts.push([last.x, last.y, last.z]); pts.push([last.x, last.y, last.z]);
@@ -456,8 +457,44 @@ impl CadCommand for PlineCommand {
.map(|t| t.as_dvec2()) .map(|t| t.as_dvec2())
.unwrap_or(DVec2::new(1.0, 0.0)); .unwrap_or(DVec2::new(1.0, 0.0));
let bulge = compute_bulge(a, tangent, b); let bulge = compute_bulge(a, tangent, b);
let arc_pts = arc_sample_points(last, bulge, pt, 16); if bulge.abs() >= 1e-9 && (b - a).length_squared() >= 1e-12 {
pts.extend_from_slice(&arc_pts); if let Some(arc) =
crate::entities::common::BulgeArc::from_bulge([a.x, a.y], [b.x, b.y], bulge)
{
for s in arc.tessellate_angle(std::f64::consts::TAU / 64.0) {
pts.push([s[0] as f32, s[1] as f32, last.z]);
}
let (sa, ea) = if arc.sweep >= 0.0 {
(arc.start_angle, arc.end_angle)
} else {
(arc.end_angle, arc.start_angle)
};
let world_center = self
.plane
.to_world(DVec3::new(arc.center[0], arc.center[1], last.z as f64));
if arc.radius > 0.0
&& arc.radius.is_finite()
&& arc.radius <= 1e6
&& sa.is_finite()
&& ea.is_finite()
{
tangent_geom = Some(TangentGeom::Arc {
center: world_center.to_array(),
axis_x: self.plane.x.to_array(),
axis_y: self.plane.y.to_array(),
radius: arc.radius,
start_angle: sa,
end_angle: ea,
});
}
} else {
let arc_pts = arc_sample_points(last, bulge, pt, 64);
pts.extend_from_slice(&arc_pts);
}
} else {
pts.push([last.x, last.y, last.z]);
pts.push([pt.x, pt.y, pt.z]);
}
} }
} }
@@ -465,12 +502,16 @@ impl CadCommand for PlineCommand {
.into_iter() .into_iter()
.map(|point| self.plane.to_world(Vec3::from_array(point).as_dvec3()).as_vec3().to_array()) .map(|point| self.plane.to_world(Vec3::from_array(point).as_dvec3()).as_vec3().to_array())
.collect(); .collect();
Some(WireModel::solid( let mut wire = WireModel::solid(
"rubber_band".into(), "rubber_band".into(),
world_points, world_points,
WireModel::CYAN, WireModel::CYAN,
false, false,
)) );
if let Some(tg) = tangent_geom {
wire.tangent_geoms.push(tg);
}
Some(wire)
} }
} }
@@ -548,4 +589,20 @@ mod tests {
CmdResult::FinalizeLiveEntity(h) if h == handle CmdResult::FinalizeLiveEntity(h) if h == handle
)); ));
} }
#[test]
fn test_draw_line_then_arc() {
let mut cmd = PlineCommand::new();
cmd.on_point(DVec3::new(0.0, 0.0, 0.0));
let res = cmd.on_point(DVec3::new(10.0, 0.0, 0.0));
assert!(matches!(res, CmdResult::CommitLiveEntity(_)));
cmd.set_live_handle(Handle::new(1));
cmd.on_text_input("A");
let wire = cmd.on_mouse_move(DVec3::new(10.0, 5.0, 0.0)).expect("arc wire");
assert_eq!(wire.tangent_geoms.len(), 1);
assert!(matches!(wire.tangent_geoms[0], TangentGeom::Arc { .. }));
assert!(wire.points.len() >= 16);
let res2 = cmd.on_point(DVec3::new(10.0, 5.0, 0.0));
assert!(matches!(res2, CmdResult::UpdateLiveEntity { .. }));
}
} }
+383 -18
View File
@@ -77,7 +77,122 @@ fn project_click(click: [f64; 2], p1: [f64; 2], unit: [f64; 2]) -> f64 {
} }
// ── Fillet ───────────────────────────────────────────────────────────────── // ── Fillet ─────────────────────────────────────────────────────────────────
/// Fillet two parallel lines with a semicircle tangent to both.
///
/// The current FILLET radius is intentionally ignored for parallel lines:
/// the effective radius is half the perpendicular distance between them.
///
/// The endpoint of the first line nearest its pick determines where the
/// semicircle is placed. The second line is trimmed or extended to the
/// corresponding perpendicular tangent point.
fn fillet_parallel_lines(
l1: &LineEnt,
click1: [f64; 2],
l2: &LineEnt,
click2: [f64; 2],
) -> Option<(EntityType, EntityType, Option<EntityType>)> {
let (p1, p2, u1, len1) = line_geom(l1);
let (p3, _p4, u2, len2) = line_geom(l2);
if len1 <= 1.0e-12 || len2 <= 1.0e-12 {
return None;
}
// This helper is only for parallel / anti-parallel lines.
let cross = u1[0] * u2[1] - u1[1] * u2[0];
if cross.abs() > 1.0e-6 {
return None;
}
// The pick on the first line determines which end receives the round.
let dist_sq = |a: [f64; 2], b: [f64; 2]| {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
dx * dx + dy * dy
};
let (tangent1, other1) = if dist_sq(click1, p1) <= dist_sq(click1, p2) {
(p1, p2)
} else {
(p2, p1)
};
// Unit normal to line 1. Project tangent1 perpendicularly onto line 2.
let normal = [-u1[1], u1[0]];
let separation =
(p3[0] - tangent1[0]) * normal[0] + (p3[1] - tangent1[1]) * normal[1];
if separation.abs() <= 1.0e-9 {
// Coincident lines do not define a useful semicircle.
return None;
}
let tangent2 = [
tangent1[0] + normal[0] * separation,
tangent1[1] + normal[1] * separation,
];
let centre = [
(tangent1[0] + tangent2[0]) * 0.5,
(tangent1[1] + tangent2[1]) * 0.5,
];
let radius = separation.abs() * 0.5;
// Keep the existing part of each selected line and trim/extend the picked
// end to its tangent point.
let new_l1 = trim_line_to_point(l1, tangent1, click1)?;
let new_l2 = trim_line_to_point(l2, tangent2, click2)?;
// The semicircle must bulge away from the retained portion of line 1.
// `other1 - tangent1` points back into the line, so negate it.
let keep = [
other1[0] - tangent1[0],
other1[1] - tangent1[1],
];
let keep_len = (keep[0] * keep[0] + keep[1] * keep[1]).sqrt();
if keep_len <= 1.0e-12 {
return None;
}
let bulge_dir = [-keep[0] / keep_len, -keep[1] / keep_len];
let a1 = norm_angle(
(tangent1[1] - centre[1]).atan2(tangent1[0] - centre[0]),
);
let a2 = norm_angle(
(tangent2[1] - centre[1]).atan2(tangent2[0] - centre[0]),
);
// Between two antipodal points there are two possible semicircles.
// Determine which CCW orientation bulges toward the selected end.
let midpoint_angle = a1 + std::f64::consts::FRAC_PI_2;
let midpoint_dir = [midpoint_angle.cos(), midpoint_angle.sin()];
let candidate_matches =
midpoint_dir[0] * bulge_dir[0] + midpoint_dir[1] * bulge_dir[1] >= 0.0;
let (start_angle, end_angle) = if candidate_matches {
(a1, a2)
} else {
(a2, a1)
};
let mut arc = ArcEnt::new();
arc.common = l1.common.clone();
arc.common.handle = Handle::NULL;
arc.center = Vector3::new(centre[0], centre[1], l1.start.z);
arc.radius = radius;
arc.start_angle = start_angle;
arc.end_angle = end_angle;
Some((
EntityType::Line(new_l1),
EntityType::Line(new_l2),
Some(EntityType::Arc(arc)),
))
}
/// Compute fillet: trim l1/l2 and insert a tangent arc of `radius`. /// Compute fillet: trim l1/l2 and insert a tangent arc of `radius`.
/// Returns (trimmed_l1, trimmed_l2, fillet_arc). /// Returns (trimmed_l1, trimmed_l2, fillet_arc).
fn compute_fillet( fn compute_fillet(
@@ -90,8 +205,18 @@ fn compute_fillet(
let (p1, p2, u1, _len1) = line_geom(l1); let (p1, p2, u1, _len1) = line_geom(l1);
let (p3, p4, u2, _len2) = line_geom(l2); let (p3, p4, u2, _len2) = line_geom(l2);
// Intersection of infinite lines // Parallel lines are a special FILLET case: there is no intersection.
let (t_p, _u_p) = ll(p1[0], p1[1], u1[0], u1[1], p3[0], p3[1], u2[0], u2[1])?; // Build the tangent semicircle directly, temporarily ignoring the
// configured radius.
let cross = u1[0] * u2[1] - u1[1] * u2[0];
if cross.abs() <= 1.0e-6 {
return fillet_parallel_lines(l1, click1, l2, click2);
}
// Intersection of infinite non-parallel lines.
let (t_p, _u_p) =
ll(p1[0], p1[1], u1[0], u1[1], p3[0], p3[1], u2[0], u2[1])?;
// Intersection point // Intersection point
let px = p1[0] + t_p * u1[0]; let px = p1[0] + t_p * u1[0];
@@ -123,14 +248,6 @@ fn compute_fillet(
[-u2[0], -u2[1]] [-u2[0], -u2[1]]
}; };
// Angle between the two keep-directions
let cos_a = (dir1[0] * dir2[0] + dir1[1] * dir2[1]).clamp(-1.0, 1.0);
let angle = cos_a.acos();
// Lines are parallel / anti-parallel
if angle < 1e-6 || (angle - std::f64::consts::PI).abs() < 1e-6 {
return None;
}
let z = l1.start.z; let z = l1.start.z;
@@ -337,7 +454,194 @@ fn compute_bulge(t1: [f64; 2], t2: [f64; 2], arc_center: [f64; 2]) -> f64 {
-(span_cw / 4.0).tan() // CW arc -(span_cw / 4.0).tan() // CW arc
} }
} }
#[derive(Clone, Copy)]
struct PolylineCornerFillet {
incoming: [f64; 2],
outgoing: [f64; 2],
bulge: f64,
}
fn segment_parameter(point: [f64; 2], start: [f64; 2], end: [f64; 2]) -> Option<f64> {
let dx = end[0] - start[0];
let dy = end[1] - start[1];
let len2 = dx * dx + dy * dy;
if len2 <= 1.0e-20 {
return None;
}
Some(
((point[0] - start[0]) * dx + (point[1] - start[1]) * dy) / len2,
)
}
/// Apply the current FILLET radius to every eligible corner of a 2D polyline.
///
/// Initial scope deliberately handles straight-segment corners only. Corners
/// adjacent to an existing bulge arc are preserved unchanged.
fn fillet_entire_lwpolyline(poly: &LwPolyline, radius: f64) -> Option<LwPolyline> {
let n = poly.vertices.len();
if n < 3 || radius <= 1.0e-9 {
return None;
}
let mut corners: Vec<Option<PolylineCornerFillet>> = vec![None; n];
let first_corner = if poly.is_closed { 0 } else { 1 };
let end_corner = if poly.is_closed {
n
} else {
n.saturating_sub(1)
};
for i in first_corner..end_corner {
let prev = if i == 0 { n - 1 } else { i - 1 };
// A bulge belongs to the segment starting at that vertex.
// For now only fillet corners where both adjacent segments are straight.
if poly.vertices[prev].bulge.abs() >= 1.0e-9
|| poly.vertices[i].bulge.abs() >= 1.0e-9
{
continue;
}
let incoming = lwpoly_seg_as_line(poly, prev);
let outgoing = lwpoly_seg_as_line(poly, i);
// Pick points halfway along each segment, away from the corner.
// This tells the existing line-line FILLET code which side to keep.
let click_in = [
(incoming.start.x + incoming.end.x) * 0.5,
(incoming.start.y + incoming.end.y) * 0.5,
];
let click_out = [
(outgoing.start.x + outgoing.end.x) * 0.5,
(outgoing.start.y + outgoing.end.y) * 0.5,
];
let Some((trimmed_in, trimmed_out, maybe_arc)) =
compute_fillet(&incoming, click_in, &outgoing, click_out, radius)
else {
continue;
};
let (
EntityType::Line(trimmed_in),
EntityType::Line(trimmed_out),
Some(EntityType::Arc(arc)),
) = (trimmed_in, trimmed_out, maybe_arc)
else {
continue;
};
let tangent_in = [trimmed_in.end.x, trimmed_in.end.y];
let tangent_out = [trimmed_out.start.x, trimmed_out.start.y];
let in_start = [incoming.start.x, incoming.start.y];
let in_end = [incoming.end.x, incoming.end.y];
let out_start = [outgoing.start.x, outgoing.start.y];
let out_end = [outgoing.end.x, outgoing.end.y];
let Some(t_in) = segment_parameter(tangent_in, in_start, in_end) else {
continue;
};
let Some(t_out) = segment_parameter(tangent_out, out_start, out_end) else {
continue;
};
// Whole-polyline FILLET should not extend past neighbouring vertices.
// If this radius cannot fit at this corner, leave that corner unchanged.
if !(-1.0e-9..=1.0 + 1.0e-9).contains(&t_in)
|| !(-1.0e-9..=1.0 + 1.0e-9).contains(&t_out)
{
continue;
}
corners[i] = Some(PolylineCornerFillet {
incoming: tangent_in,
outgoing: tangent_out,
bulge: compute_bulge(
tangent_in,
tangent_out,
[arc.center.x, arc.center.y],
),
});
}
if corners.iter().all(Option::is_none) {
return None;
}
// Adjacent fillets must not consume more than the shared segment.
// Reject the operation rather than producing an inverted/overlapping edge.
let segment_count = if poly.is_closed { n } else { n - 1 };
for i in 0..segment_count {
let next = (i + 1) % n;
let start = corners[i]
.map(|corner| corner.outgoing)
.unwrap_or([
poly.vertices[i].location.x,
poly.vertices[i].location.y,
]);
let end = corners[next]
.map(|corner| corner.incoming)
.unwrap_or([
poly.vertices[next].location.x,
poly.vertices[next].location.y,
]);
let original_start = [
poly.vertices[i].location.x,
poly.vertices[i].location.y,
];
let original_end = [
poly.vertices[next].location.x,
poly.vertices[next].location.y,
];
let Some(start_t) = segment_parameter(start, original_start, original_end) else {
continue;
};
let Some(end_t) = segment_parameter(end, original_start, original_end) else {
continue;
};
if start_t > end_t + 1.0e-9 {
return None;
}
}
let mut result = poly.clone();
result.common.handle = Handle::NULL;
result.vertices.clear();
for (i, original) in poly.vertices.iter().enumerate() {
if let Some(corner) = corners[i] {
let mut tangent_in = original.clone();
tangent_in.location.x = corner.incoming[0];
tangent_in.location.y = corner.incoming[1];
tangent_in.bulge = corner.bulge;
let mut tangent_out = original.clone();
tangent_out.location.x = corner.outgoing[0];
tangent_out.location.y = corner.outgoing[1];
// This corner is only eligible when its outgoing segment is straight.
tangent_out.bulge = 0.0;
result.vertices.push(tangent_in);
result.vertices.push(tangent_out);
} else {
result.vertices.push(original.clone());
}
}
Some(result)
}
/// Rebuild a LwPolyline, replacing the corner vertex at `corner_idx` with two /// Rebuild a LwPolyline, replacing the corner vertex at `corner_idx` with two
/// new vertices T1 (start of fillet arc) and T2 (end of fillet arc). /// new vertices T1 (start of fillet arc) and T2 (end of fillet arc).
/// `bulge` encodes the direction and span of the arc (T1 → T2). /// `bulge` encodes the direction and span of the arc (T1 → T2).
@@ -991,6 +1295,7 @@ fn compute_chamfer(
enum FilletStep { enum FilletStep {
First, First,
Polyline,
WaitingForRadius, WaitingForRadius,
Second { Second {
h1: Handle, h1: Handle,
@@ -1072,7 +1377,12 @@ impl CadCommand for FilletCommand {
fn prompt(&self) -> String { fn prompt(&self) -> String {
match &self.step { match &self.step {
FilletStep::First => crate::tf!( FilletStep::First => crate::tf!(
"FILLET Select first object (Line/Arc/LwPolyline) [R={:.4}]:", "FILLET Select first object or [Polyline/Radius] [R={:.4}]:",
self.radius
)
.into_owned(),
FilletStep::Polyline => crate::tf!(
"FILLET Select 2D polyline [R={:.4}]:",
self.radius self.radius
) )
.into_owned(), .into_owned(),
@@ -1091,11 +1401,16 @@ impl CadCommand for FilletCommand {
fn options(&self) -> Vec<crate::command::CmdOption> { fn options(&self) -> Vec<crate::command::CmdOption> {
use crate::command::CmdOption; use crate::command::CmdOption;
match self.step { match self.step {
// Offer the radius keyword while selecting objects; the "R" token is FilletStep::First => vec![
// already handled by on_text_input. CmdOption::new("Polyline", "P"),
FilletStep::First | FilletStep::Second { .. } => vec![CmdOption::new("Radius", "R")], CmdOption::new("Radius", "R"),
FilletStep::WaitingForRadius => vec![], ],
FilletStep::Second { .. } => {
vec![CmdOption::new("Radius", "R")]
}
FilletStep::Polyline | FilletStep::WaitingForRadius => vec![],
} }
} }
@@ -1134,6 +1449,10 @@ impl CadCommand for FilletCommand {
FilletStep::First | FilletStep::Second { .. } => { FilletStep::First | FilletStep::Second { .. } => {
let t = text.trim(); let t = text.trim();
let upper = t.to_uppercase(); let upper = t.to_uppercase();
if matches!(self.step, FilletStep::First) && upper == "P" {
self.step = FilletStep::Polyline;
return Some(CmdResult::NeedPoint);
}
// "R" alone → enter sub-step to collect radius // "R" alone → enter sub-step to collect radius
if upper == "R" { if upper == "R" {
self.enter_radius_substep(); self.enter_radius_substep();
@@ -1156,6 +1475,7 @@ impl CadCommand for FilletCommand {
} }
None None
} }
FilletStep::Polyline => None,
} }
} }
@@ -1171,6 +1491,28 @@ impl CadCommand for FilletCommand {
match &self.step { match &self.step {
FilletStep::WaitingForRadius => return CmdResult::NeedPoint, FilletStep::WaitingForRadius => return CmdResult::NeedPoint,
FilletStep::Polyline => {
let poly = self
.entity_index
.get(&self.all_entities, handle)
.and_then(|entity| match entity {
EntityType::LwPolyline(poly) => Some(poly.clone()),
_ => None,
});
let Some(poly) = poly else {
return CmdResult::NeedPoint;
};
let Some(result) = fillet_entire_lwpolyline(&poly, self.radius) else {
return CmdResult::NeedPoint;
};
self.continue_after_fillet(vec![(
handle,
vec![EntityType::LwPolyline(result)],
)])
}
FilletStep::First => { FilletStep::First => {
let e1 = self let e1 = self
.entity_index .entity_index
@@ -1245,6 +1587,28 @@ impl CadCommand for FilletCommand {
match &self.step { match &self.step {
FilletStep::WaitingForRadius => vec![], FilletStep::WaitingForRadius => vec![],
FilletStep::Polyline => {
let preview = self
.entity_index
.get(&self.all_entities, handle)
.and_then(|entity| match entity {
EntityType::LwPolyline(poly) => {
fillet_entire_lwpolyline(poly, self.radius)
}
_ => None,
});
preview
.map(|poly| {
vec![WireModel::solid(
"fillet_polyline_preview".into(),
lwpoly_pts(&poly),
WireModel::CYAN,
false,
)]
})
.unwrap_or_default()
}
FilletStep::First => { FilletStep::First => {
let pts = self let pts = self
.entity_index .entity_index
@@ -1515,12 +1879,13 @@ impl CadCommand for ChamferCommand {
fn options(&self) -> Vec<crate::command::CmdOption> { fn options(&self) -> Vec<crate::command::CmdOption> {
use crate::command::CmdOption; use crate::command::CmdOption;
match self.step { match self.step {
// Offer the distance keyword while selecting objects; the "D" token
// is already handled by on_text_input.
ChamferStep::First ChamferStep::First
| ChamferStep::Second { .. } | ChamferStep::Second { .. }
| ChamferStep::SecondPoly { .. } => vec![CmdOption::new(t!("Distance").as_ref(), "D")], | ChamferStep::SecondPoly { .. } => {
vec![CmdOption::new(t!("Distance").as_ref(), "D")]
}
ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 => vec![], ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 => vec![],
} }
} }
+24 -3
View File
@@ -17,6 +17,7 @@ use crate::modules::draw::fence::FencePick;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
pub struct SelectObjectsCommand { pub struct SelectObjectsCommand {
prompt_cmd: String,
pending_cmd: String, pending_cmd: String,
/// Selection accumulated so far (kept in sync with the scene selection by /// Selection accumulated so far (kept in sync with the scene selection by
/// each `on_selection_complete` call). Applied on Enter when `commit_on_enter`. /// each `on_selection_complete` call). Applied on Enter when `commit_on_enter`.
@@ -24,6 +25,8 @@ pub struct SelectObjectsCommand {
/// When true, gathering continues until Enter / right-click commits the set. /// When true, gathering continues until Enter / right-click commits the set.
/// When false, the first completed selection action fires immediately. /// When false, the first completed selection action fires immediately.
commit_on_enter: bool, commit_on_enter: bool,
/// Whether the generic selection methods are shown as command buttons.
show_options: bool,
/// A Fence / WPolygon / CPolygon path being picked point by point. While /// A Fence / WPolygon / CPolygon path being picked point by point. While
/// one is under way the host must send clicks here as points rather than /// one is under way the host must send clicks here as points rather than
/// treating them as picks, which is what `is_selection_gathering` reports. /// treating them as picks, which is what `is_selection_gathering` reports.
@@ -37,9 +40,25 @@ impl SelectObjectsCommand {
/// Standard selection set: accumulate picks, apply on Enter / right-click. /// Standard selection set: accumulate picks, apply on Enter / right-click.
pub fn new(pending_cmd: &str) -> Self { pub fn new(pending_cmd: &str) -> Self {
Self { Self {
prompt_cmd: pending_cmd.to_string(),
pending_cmd: pending_cmd.to_string(), pending_cmd: pending_cmd.to_string(),
handles: Vec::new(), handles: Vec::new(),
commit_on_enter: true, commit_on_enter: true,
show_options: true,
pick: None,
pick_crossing: true,
}
}
/// Plain gather prompt with no generic selection-method buttons. The
/// visible command name may differ from the private apply command.
pub fn plain(prompt_cmd: &str, pending_cmd: &str) -> Self {
Self {
prompt_cmd: prompt_cmd.to_string(),
pending_cmd: pending_cmd.to_string(),
handles: Vec::new(),
commit_on_enter: true,
show_options: false,
pick: None, pick: None,
pick_crossing: true, pick_crossing: true,
} }
@@ -49,9 +68,11 @@ impl SelectObjectsCommand {
/// immediately, with no Enter (used by commands that act on one object). /// immediately, with no Enter (used by commands that act on one object).
pub fn instant(pending_cmd: &str) -> Self { pub fn instant(pending_cmd: &str) -> Self {
Self { Self {
prompt_cmd: pending_cmd.to_string(),
pending_cmd: pending_cmd.to_string(), pending_cmd: pending_cmd.to_string(),
handles: Vec::new(), handles: Vec::new(),
commit_on_enter: false, commit_on_enter: false,
show_options: false,
pick: None, pick: None,
pick_crossing: true, pick_crossing: true,
} }
@@ -82,12 +103,12 @@ impl CadCommand for SelectObjectsCommand {
if self.commit_on_enter && !self.handles.is_empty() { if self.commit_on_enter && !self.handles.is_empty() {
t!( t!(
"%{cmd} Select objects (%{count} selected, Enter to apply):", "%{cmd} Select objects (%{count} selected, Enter to apply):",
cmd = self.pending_cmd, cmd = self.prompt_cmd,
count = self.handles.len() count = self.handles.len()
) )
.into_owned() .into_owned()
} else { } else {
t!("%{cmd} Select objects:", cmd = self.pending_cmd).into_owned() t!("%{cmd} Select objects:", cmd = self.prompt_cmd).into_owned()
} }
} }
@@ -108,7 +129,7 @@ impl CadCommand for SelectObjectsCommand {
// they work typed as well — these buttons only surface them, which is what // they work typed as well — these buttons only surface them, which is what
// the on-screen keyboard-less case needs. // the on-screen keyboard-less case needs.
fn options(&self) -> Vec<CmdOption> { fn options(&self) -> Vec<CmdOption> {
if self.commit_on_enter { if self.commit_on_enter && self.show_options {
vec![ vec![
CmdOption::new(t!("Window").as_ref(), "W"), CmdOption::new(t!("Window").as_ref(), "W"),
CmdOption::new(t!("Crossing").as_ref(), "C"), CmdOption::new(t!("Crossing").as_ref(), "C"),
+7 -6
View File
@@ -2211,7 +2211,7 @@ impl LoftCommand {
fn option_prompt(&self) -> String { fn option_prompt(&self) -> String {
match self.state.step { match self.state.step {
LoftStep::Sections => format!( LoftStep::Sections => format!(
"{} ({}):", t!("LOFT Select cross-sections in order or [Point/Join/Mode/Undo] (Enter to finish)"), "{} ({}):", t!("LOFT Select cross-sections in order or [Point/Join/Mode] (Enter to finish)"),
self.state.sections.len(), self.state.sections.len(),
), ),
LoftStep::Join => format!( LoftStep::Join => format!(
@@ -2224,9 +2224,9 @@ impl LoftCommand {
LoftStep::Options => { LoftStep::Options => {
let (start, end) = self.point_ends(); let (start, end) = self.point_ends();
if start || end { if start || end {
t!("LOFT Enter an option [Guides/Path/Cross sections only/Settings/Continuity/Bulge magnitude/Mode/Undo] <Cross sections only>:").into_owned() t!("LOFT Enter an option [Guides/Path/Cross sections only/Settings/Continuity/Bulge magnitude] <Cross sections only>:").into_owned()
} else { } else {
t!("LOFT Enter an option [Guides/Path/Cross sections only/Settings/Mode/Undo] <Cross sections only>:").into_owned() t!("LOFT Enter an option [Guides/Path/Cross sections only/Settings] <Cross sections only>:").into_owned()
} }
} }
LoftStep::Guides => format!("{} ({}):", t!("LOFT Select guide curves or [Undo] (Enter to finish)"), self.state.guides.len()), LoftStep::Guides => format!("{} ({}):", t!("LOFT Select guide curves or [Undo] (Enter to finish)"), self.state.guides.len()),
@@ -2272,7 +2272,7 @@ impl CadCommand for LoftCommand {
} }
fn options(&self) -> Vec<CmdOption> { fn options(&self) -> Vec<CmdOption> {
let mut options = match self.state.step { let mut options = match self.state.step {
LoftStep::Sections => vec![CmdOption::new("Point", "POINT"), CmdOption::new("Join", "JOIN"), CmdOption::new("Mode", "MODE"), CmdOption::enter("Done")], LoftStep::Sections => vec![CmdOption::new("Point", "POINT"), CmdOption::new("Join", "JOIN"), CmdOption::new("Mode", "MODE")],
LoftStep::Join | LoftStep::Guides => vec![CmdOption::enter("Done")], LoftStep::Join | LoftStep::Guides => vec![CmdOption::enter("Done")],
LoftStep::Mode => vec![CmdOption::new("Solid", "SOLID"), CmdOption::new("Surface", "SURFACE")], LoftStep::Mode => vec![CmdOption::new("Solid", "SOLID"), CmdOption::new("Surface", "SURFACE")],
LoftStep::Options => { LoftStep::Options => {
@@ -2282,7 +2282,6 @@ impl CadCommand for LoftCommand {
choices.push(CmdOption::new("Continuity", "CONTINUITY")); choices.push(CmdOption::new("Continuity", "CONTINUITY"));
choices.push(CmdOption::new("Bulge magnitude", "BULGE")); choices.push(CmdOption::new("Bulge magnitude", "BULGE"));
} }
choices.push(CmdOption::new("Mode", "MODE"));
choices choices
} }
LoftStep::Settings => { LoftStep::Settings => {
@@ -2308,7 +2307,9 @@ impl CadCommand for LoftCommand {
LoftStep::StartContinuity | LoftStep::EndContinuity => vec![CmdOption::new("G0", "G0"), CmdOption::new("G1", "G1")], LoftStep::StartContinuity | LoftStep::EndContinuity => vec![CmdOption::new("G0", "G0"), CmdOption::new("G1", "G1")],
_ => Vec::new(), _ => Vec::new(),
}; };
options.push(CmdOption::new("Undo", "UNDO")); if !matches!(self.state.step, LoftStep::Sections | LoftStep::Options) {
options.push(CmdOption::new("Undo", "UNDO"));
}
options options
} }
fn needs_entity_pick(&self) -> bool { fn needs_entity_pick(&self) -> bool {
+80 -9
View File
@@ -25,16 +25,20 @@ impl BoolOp {
enum SubtractStep { enum SubtractStep {
Bases, Bases,
Cutters, Cutters,
ConvertMeshes,
} }
pub struct SubtractCommand { pub struct SubtractCommand {
step: SubtractStep, step: SubtractStep,
bases: Vec<Handle>, bases: Vec<Handle>,
cutters: Vec<Handle>,
selected: Vec<Handle>, selected: Vec<Handle>,
bases_have_mesh: bool,
selected_has_mesh: bool,
} }
impl SubtractCommand { impl SubtractCommand {
pub fn new(bases: Vec<Handle>) -> Self { pub fn new(bases: Vec<Handle>, bases_have_mesh: bool) -> Self {
let step = if bases.is_empty() { let step = if bases.is_empty() {
SubtractStep::Bases SubtractStep::Bases
} else { } else {
@@ -43,7 +47,18 @@ impl SubtractCommand {
Self { Self {
step, step,
bases, bases,
cutters: Vec::new(),
selected: Vec::new(), selected: Vec::new(),
bases_have_mesh,
selected_has_mesh: false,
}
}
fn finish(&mut self, convert_meshes: bool) -> CmdResult {
CmdResult::SolidSubtract {
bases: std::mem::take(&mut self.bases),
cutters: std::mem::take(&mut self.cutters),
convert_meshes,
} }
} }
} }
@@ -55,11 +70,26 @@ impl CadCommand for SubtractCommand {
fn prompt(&self) -> String { fn prompt(&self) -> String {
match self.step { match self.step {
SubtractStep::Bases => crate::t!("SUBTRACT Select base solids, then press Enter:") SubtractStep::Bases => crate::t!("SUBTRACT Select base Solids, Regions, Surfaces, or Meshes, then press Enter:")
.into_owned(), .into_owned(),
SubtractStep::Cutters => { SubtractStep::Cutters => {
crate::t!("SUBTRACT Select solids to subtract, then press Enter:").into_owned() crate::t!("SUBTRACT Select Solids, Regions, Surfaces, or Meshes to subtract, then press Enter:").into_owned()
} }
SubtractStep::ConvertMeshes => crate::t!(
"SUBTRACT Convert selected closed Mesh objects to solids? [Yes/No] <Yes>:"
)
.into_owned(),
}
}
fn options(&self) -> Vec<crate::command::CmdOption> {
if matches!(self.step, SubtractStep::ConvertMeshes) {
vec![
crate::command::CmdOption::new(crate::t!("Yes").as_ref(), "Y"),
crate::command::CmdOption::new(crate::t!("No").as_ref(), "N"),
]
} else {
Vec::new()
} }
} }
@@ -68,31 +98,72 @@ impl CadCommand for SubtractCommand {
} }
fn on_enter(&mut self) -> CmdResult { fn on_enter(&mut self) -> CmdResult {
if matches!(self.step, SubtractStep::ConvertMeshes) {
return self.finish(true);
}
if self.selected.is_empty() { if self.selected.is_empty() {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
match self.step { match self.step {
SubtractStep::Bases => { SubtractStep::Bases => {
self.bases = std::mem::take(&mut self.selected); self.bases = std::mem::take(&mut self.selected);
self.bases_have_mesh = self.selected_has_mesh;
self.selected_has_mesh = false;
self.step = SubtractStep::Cutters; self.step = SubtractStep::Cutters;
CmdResult::DeselectAndContinue CmdResult::DeselectAndContinue
} }
SubtractStep::Cutters => CmdResult::SolidSubtract { SubtractStep::Cutters => {
bases: std::mem::take(&mut self.bases), self.cutters = std::mem::take(&mut self.selected);
cutters: std::mem::take(&mut self.selected), if self.bases_have_mesh || self.selected_has_mesh {
}, self.step = SubtractStep::ConvertMeshes;
CmdResult::NeedPoint
} else {
self.finish(false)
}
}
SubtractStep::ConvertMeshes => unreachable!(),
} }
} }
fn wants_text_input(&self) -> bool {
matches!(self.step, SubtractStep::ConvertMeshes)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if !matches!(self.step, SubtractStep::ConvertMeshes) {
return None;
}
Some(match text.trim().to_ascii_lowercase().as_str() {
"" | "y" | "yes" => self.finish(true),
"n" | "no" => self.finish(false),
_ => CmdResult::NeedPoint,
})
}
fn is_selection_gathering(&self) -> bool { fn is_selection_gathering(&self) -> bool {
true !matches!(self.step, SubtractStep::ConvertMeshes)
} }
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) { fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
self.selected_has_mesh = entities.iter().any(|entity| {
matches!(
entity.entity,
EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_)
)
});
self.selected = entities self.selected = entities
.into_iter() .into_iter()
.filter_map(|entity| { .filter_map(|entity| {
matches!(entity.entity, EntityType::Solid3D(_)).then_some(entity.handle) matches!(
entity.entity,
EntityType::Solid3D(_)
| EntityType::Region(_)
| EntityType::Surface(_)
| EntityType::Mesh(_)
| EntityType::PolygonMesh(_)
| EntityType::PolyfaceMesh(_)
)
.then_some(entity.handle)
}) })
.collect(); .collect();
} }
+781 -43
View File
@@ -1,42 +1,311 @@
use std::sync::atomic::{AtomicU64, Ordering};
use acadrust::Handle; use acadrust::Handle;
use cadkernel::brep::{Body, EdgeKey, FaceKey};
use glam::DVec3; use glam::DVec3;
use crate::command::{ use crate::command::{
CadCommand, CmdResult, DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec, CadCommand, CmdOption, CmdResult, DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec,
}; };
use crate::scene::model::wire_model::WireModel;
#[derive(Clone, Copy)] static FILLET_RADIUS_BITS: AtomicU64 = AtomicU64::new(1.0f64.to_bits());
fn fillet_radius() -> f64 {
f64::from_bits(FILLET_RADIUS_BITS.load(Ordering::Relaxed))
}
fn set_fillet_radius(value: f64) {
FILLET_RADIUS_BITS.store(value.to_bits(), Ordering::Relaxed);
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum EdgeOperation { pub enum EdgeOperation {
Fillet, Fillet,
Chamfer, Chamfer,
} }
#[derive(Clone, Copy, PartialEq, Eq)]
enum EdgeSelectionMode {
Edge,
Chain,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum EdgeStep {
Selecting,
PickingLoop,
LoopConfirm,
PreviewConfirm,
Radius,
Expression,
ChamferDistance1,
ChamferDistance2,
}
pub struct SolidEdgeCommand { pub struct SolidEdgeCommand {
operation: EdgeOperation, operation: EdgeOperation,
target: Option<Handle>, target: Option<Handle>,
picked: Option<(Handle, DVec3)>, bodies: Vec<(Handle, Body)>,
handle: Option<Handle>,
selected_edges: Vec<EdgeKey>,
base_face: Option<FaceKey>,
selection_batches: Vec<(Vec<EdgeKey>, DVec3)>,
selection_mode: EdgeSelectionMode,
step: EdgeStep,
value_return: EdgeStep,
expression_return: EdgeStep,
loop_candidates: Vec<(FaceKey, Vec<EdgeKey>)>,
loop_index: usize,
last_pick: Option<DVec3>,
default_value: f64, default_value: f64,
other_value: f64,
preview_color: [f32; 4],
preview_wires: Vec<WireModel>,
preview_hidden: Vec<Handle>,
} }
impl SolidEdgeCommand { impl SolidEdgeCommand {
pub fn new(operation: EdgeOperation, target: Option<Handle>) -> Self { pub fn new(
operation: EdgeOperation,
target: Option<Handle>,
bodies: Vec<(Handle, Body)>,
preview_color: [f32; 4],
) -> Self {
Self { Self {
operation, operation,
target, target,
picked: None, bodies,
default_value: 1.0, handle: None,
selected_edges: Vec::new(),
base_face: None,
selection_batches: Vec::new(),
selection_mode: EdgeSelectionMode::Edge,
step: EdgeStep::Selecting,
value_return: EdgeStep::Selecting,
expression_return: EdgeStep::Radius,
loop_candidates: Vec::new(),
loop_index: 0,
last_pick: None,
default_value: if operation == EdgeOperation::Fillet {
fillet_radius()
} else {
1.0
},
other_value: 1.0,
preview_color,
preview_wires: Vec::new(),
preview_hidden: Vec::new(),
} }
} }
fn finish(&self, value: f64) -> CmdResult { pub fn new_chamfer(
let Some((handle, pick)) = self.picked else { target: Option<Handle>,
bodies: Vec<(Handle, Body)>,
preview_color: [f32; 4],
distances: (f64, f64),
) -> Self {
let mut command = Self::new(EdgeOperation::Chamfer, target, bodies, preview_color);
command.default_value = positive_default(distances.0);
command.other_value = positive_default(distances.1);
command
}
pub fn chamfer_distances(&self) -> (f64, f64) {
(self.default_value, self.other_value)
}
fn body(&self, handle: Handle) -> Option<&Body> {
self.bodies
.iter()
.find_map(|(candidate, body)| (*candidate == handle).then_some(body))
}
fn active_body(&self) -> Option<(Handle, &Body)> {
let handle = self.handle?;
self.body(handle).map(|body| (handle, body))
}
fn pick_allowed(&self, handle: Handle) -> bool {
!handle.is_null()
&& self.target.is_none_or(|target| target == handle)
&& self.handle.is_none_or(|selected| selected == handle)
&& self.body(handle).is_some()
}
fn add_batch(&mut self, edges: Vec<EdgeKey>, anchor: DVec3) -> bool {
let batch = edges
.into_iter()
.filter(|edge| !self.selected_edges.contains(edge))
.collect::<Vec<_>>();
if batch.is_empty() {
return false;
}
self.selected_edges.extend(batch.iter().copied());
self.selection_batches.push((batch, anchor));
true
}
fn current_loop(&self) -> Option<&[EdgeKey]> {
self.loop_candidates
.get(self.loop_index)
.map(|(_, edges)| edges.as_slice())
}
fn current_loop_face(&self) -> Option<FaceKey> {
self.loop_candidates
.get(self.loop_index)
.map(|(face, _)| *face)
}
fn preview_edges(&self) -> Vec<EdgeKey> {
let mut edges = self.selected_edges.clone();
if self.step == EdgeStep::LoopConfirm {
if let Some(loop_edges) = self.current_loop() {
for edge in loop_edges {
if !edges.contains(edge) {
edges.push(*edge);
}
}
}
}
edges
}
fn preview_active(&self) -> bool {
matches!(
self.step,
EdgeStep::Selecting
| EdgeStep::PickingLoop
| EdgeStep::LoopConfirm
| EdgeStep::PreviewConfirm
| EdgeStep::Radius
| EdgeStep::Expression
| EdgeStep::ChamferDistance1
| EdgeStep::ChamferDistance2
)
}
fn preview_for_values(
&self,
value: f64,
other_value: f64,
) -> Option<(Handle, Vec<WireModel>)> {
if !self.preview_active() {
return None;
}
let edges = self.preview_edges();
if edges.is_empty() {
return None;
}
let (handle, body) = self.active_body()?;
let result = match self.operation {
EdgeOperation::Fillet => cadkernel::brep::fillet_edges(body, &edges, value).ok()?,
EdgeOperation::Chamfer => cadkernel::brep::chamfer_edges(
body,
&edges,
self.base_face.or_else(|| self.current_loop_face())?,
value,
other_value,
)
.ok()?,
};
let mut wires = crate::scene::model::solid_model::grip_preview_wires(&result, handle);
for wire in &mut wires {
wire.color = self.preview_color;
wire.name = format!("{}-{}-PREVIEW", handle.value(), self.name());
}
(!wires.is_empty()).then_some((handle, wires))
}
fn rebuild_preview(&mut self) {
self.preview_wires.clear();
self.preview_hidden.clear();
if let Some((handle, wires)) =
self.preview_for_values(self.default_value, self.other_value)
{
self.preview_wires = wires;
self.preview_hidden.push(handle);
}
}
fn begin_radius(&mut self, return_to: EdgeStep) -> CmdResult {
self.value_return = return_to;
self.step = EdgeStep::Radius;
self.rebuild_preview();
CmdResult::NeedPoint
}
fn accept_radius(&mut self, value: f64) -> Option<CmdResult> {
if value <= 0.0 || !value.is_finite() {
return None;
}
self.default_value = value;
if self.operation == EdgeOperation::Fillet {
set_fillet_radius(value);
}
self.step = self.value_return;
self.rebuild_preview();
Some(CmdResult::NeedPoint)
}
fn begin_distances(&mut self, return_to: EdgeStep) -> CmdResult {
self.value_return = return_to;
self.step = EdgeStep::ChamferDistance1;
self.rebuild_preview();
CmdResult::NeedPoint
}
fn accept_distance(&mut self, value: f64) -> Option<CmdResult> {
if value <= 0.0 || !value.is_finite() {
return None;
}
match self.step {
EdgeStep::ChamferDistance1 => {
self.default_value = value;
self.step = EdgeStep::ChamferDistance2;
}
EdgeStep::ChamferDistance2 => {
self.other_value = value;
self.step = self.value_return;
}
_ => return None,
}
self.rebuild_preview();
Some(CmdResult::NeedPoint)
}
fn accept_loop(&mut self) -> CmdResult {
if let (Some(edges), Some(anchor)) = (
self.current_loop().map(|edges| edges.to_vec()),
self.last_pick,
) {
if self.operation == EdgeOperation::Chamfer && self.base_face.is_none() {
self.base_face = self.current_loop_face();
}
self.add_batch(edges, anchor);
}
self.loop_candidates.clear();
self.loop_index = 0;
self.step = EdgeStep::Selecting;
self.rebuild_preview();
CmdResult::NeedPoint
}
fn finish(&self) -> CmdResult {
let Some(handle) = self.handle else {
return CmdResult::Cancel; return CmdResult::Cancel;
}; };
if self.selected_edges.is_empty() {
return CmdResult::Cancel;
}
CmdResult::SolidEdgeBlend { CmdResult::SolidEdgeBlend {
handle, handle,
pick, edges: self.selected_edges.clone(),
value, base_face: self.base_face,
fillet: matches!(self.operation, EdgeOperation::Fillet), value: self.default_value,
other_value: self.other_value,
fillet: self.operation == EdgeOperation::Fillet,
} }
} }
} }
@@ -44,27 +313,114 @@ impl SolidEdgeCommand {
impl CadCommand for SolidEdgeCommand { impl CadCommand for SolidEdgeCommand {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
match self.operation { match self.operation {
EdgeOperation::Fillet => "SOLIDFILLET", EdgeOperation::Fillet => "FILLETEDGE",
EdgeOperation::Chamfer => "SOLIDCHAMFER", EdgeOperation::Chamfer => "CHAMFEREDGE",
} }
} }
fn prompt(&self) -> String { fn prompt(&self) -> String {
if self.picked.is_none() { match (self.operation, self.step, self.selection_mode) {
return crate::t!("Select a solid edge:").into_owned(); (EdgeOperation::Chamfer, EdgeStep::Selecting, _) => {
crate::t!("Select an edge or [Loop/Distance]:").into_owned()
}
(EdgeOperation::Chamfer, EdgeStep::PickingLoop, _) => {
crate::t!("Select edge of loop or [Edge/Distance]:").into_owned()
}
(EdgeOperation::Chamfer, EdgeStep::PreviewConfirm, _) => {
crate::t!("Press Enter to accept the chamfer or [Distance]:").into_owned()
}
(EdgeOperation::Chamfer, EdgeStep::ChamferDistance1, _) => crate::tf!(
"Specify Distance1 or [Expression] <{:.4}>:",
self.default_value
)
.into_owned(),
(EdgeOperation::Chamfer, EdgeStep::ChamferDistance2, _) => crate::tf!(
"Specify Distance2 or [Expression] <{:.4}>:",
self.other_value
)
.into_owned(),
(_, EdgeStep::Selecting, EdgeSelectionMode::Edge) => {
crate::t!("Select an edge or [Chain/Loop/Radius]:").into_owned()
}
(_, EdgeStep::Selecting, EdgeSelectionMode::Chain) => {
crate::t!("Select an edge chain or [Edge/Radius]:").into_owned()
}
(_, EdgeStep::PickingLoop, _) => {
crate::t!("Select edge of loop or [Edge/Chain/Radius]:").into_owned()
}
(_, EdgeStep::LoopConfirm, _) => {
crate::t!("Enter an option [Accept/Next] <Accept>:").into_owned()
}
(_, EdgeStep::PreviewConfirm, _) => {
crate::t!("Press Enter to accept the fillet or [Radius]:").into_owned()
}
(_, EdgeStep::Radius, _) if self.value_return == EdgeStep::PreviewConfirm => crate::tf!(
"Specify Radius or [Expression] <{:.4}>:",
self.default_value
)
.into_owned(),
(_, EdgeStep::Radius, _) => crate::tf!(
"Enter fillet radius or [Expression] <{:.4}>:",
self.default_value
)
.into_owned(),
(_, EdgeStep::Expression, _) => crate::t!("Enter expression:").into_owned(),
_ => String::new(),
} }
match self.operation { }
EdgeOperation::Fillet => {
crate::tf!("Specify fillet radius <{:.3}>:", self.default_value).into_owned() fn options(&self) -> Vec<CmdOption> {
match (self.operation, self.step, self.selection_mode) {
(EdgeOperation::Chamfer, EdgeStep::Selecting, _) => vec![
CmdOption::new("Loop", "L"),
CmdOption::new("Distance", "D"),
],
(EdgeOperation::Chamfer, EdgeStep::PickingLoop, _) => vec![
CmdOption::new("Edge", "E"),
CmdOption::new("Distance", "D"),
],
(EdgeOperation::Chamfer, EdgeStep::LoopConfirm, _) => vec![
CmdOption::new("Accept", "A"),
CmdOption::new("Next", "N"),
],
(EdgeOperation::Chamfer, EdgeStep::PreviewConfirm, _) => {
vec![CmdOption::new("Distance", "D")]
} }
EdgeOperation::Chamfer => { (
crate::tf!("Specify chamfer distance <{:.3}>:", self.default_value).into_owned() EdgeOperation::Chamfer,
EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2,
_,
) => vec![CmdOption::new("Expression", "E")],
(EdgeOperation::Fillet, EdgeStep::Selecting, EdgeSelectionMode::Edge) => vec![
CmdOption::new("Chain", "C"),
CmdOption::new("Loop", "L"),
CmdOption::new("Radius", "R"),
],
(EdgeOperation::Fillet, EdgeStep::Selecting, EdgeSelectionMode::Chain) => vec![
CmdOption::new("Edge", "E"),
CmdOption::new("Radius", "R"),
],
(EdgeOperation::Fillet, EdgeStep::PickingLoop, _) => vec![
CmdOption::new("Edge", "E"),
CmdOption::new("Chain", "C"),
CmdOption::new("Radius", "R"),
],
(EdgeOperation::Fillet, EdgeStep::LoopConfirm, _) => vec![
CmdOption::new("Accept", "A"),
CmdOption::new("Next", "N"),
],
(EdgeOperation::Fillet, EdgeStep::PreviewConfirm, _) => {
vec![CmdOption::new("Radius", "R")]
} }
(EdgeOperation::Fillet, EdgeStep::Radius, _) => {
vec![CmdOption::new("Expression", "E")]
}
_ => Vec::new(),
} }
} }
fn needs_entity_pick(&self) -> bool { fn needs_entity_pick(&self) -> bool {
self.picked.is_none() matches!(self.step, EdgeStep::Selecting | EdgeStep::PickingLoop)
} }
fn entity_pick_includes_fills(&self) -> bool { fn entity_pick_includes_fills(&self) -> bool {
@@ -80,52 +436,281 @@ impl CadCommand for SolidEdgeCommand {
} }
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
if handle.is_null() || self.target.is_some_and(|target| target != handle) { if !self.pick_allowed(handle) {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
self.picked = Some((handle, point)); let Some(seed) = self
.body(handle)
.and_then(|body| crate::scene::model::solid_model::nearest_edge(body, point.to_array()))
else {
return CmdResult::NeedPoint;
};
self.handle = Some(handle);
self.last_pick = Some(point);
if self.step == EdgeStep::PickingLoop {
self.loop_candidates = edge_loops(self.body(handle).expect("pick body exists"), seed);
if self.operation == EdgeOperation::Chamfer {
if let Some(base_face) = self.base_face {
self.loop_candidates
.retain(|(face, _)| *face == base_face);
}
}
self.loop_index = 0;
self.step = if self.loop_candidates.is_empty() {
EdgeStep::Selecting
} else {
EdgeStep::LoopConfirm
};
self.rebuild_preview();
return CmdResult::NeedPoint;
}
if self.operation == EdgeOperation::Chamfer {
if self.base_face.is_none() {
self.base_face = self.body(handle).and_then(|body| {
nearest_edge_face(body, seed, point.to_array())
});
}
let Some(base_face) = self.base_face else {
return CmdResult::NeedPoint;
};
if !edge_belongs_to_face(self.body(handle).expect("pick body exists"), seed, base_face)
{
return CmdResult::NeedPoint;
}
self.add_batch(vec![seed], point);
self.rebuild_preview();
return CmdResult::NeedPoint;
}
let edges = match self.selection_mode {
EdgeSelectionMode::Edge => vec![seed],
EdgeSelectionMode::Chain => edge_chain(self.body(handle).expect("pick body exists"), seed),
};
self.add_batch(edges, point);
self.rebuild_preview();
CmdResult::NeedPoint CmdResult::NeedPoint
} }
fn on_point(&mut self, point: DVec3) -> CmdResult { fn on_point(&mut self, point: DVec3) -> CmdResult {
let Some((_, pick)) = self.picked else { if !matches!(
self.step,
EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2
) {
return CmdResult::NeedPoint;
}
let Some(anchor) = self.last_pick else {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
}; };
let value = point.distance(pick); let value = point.distance(anchor);
if value > 0.0 && value.is_finite() { if value <= 0.0 || !value.is_finite() {
self.finish(value) return CmdResult::NeedPoint;
} else { }
CmdResult::NeedPoint match self.step {
EdgeStep::Radius => self.accept_radius(value).unwrap_or(CmdResult::NeedPoint),
EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 => {
self.accept_distance(value).unwrap_or(CmdResult::NeedPoint)
}
_ => CmdResult::NeedPoint,
} }
} }
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if self.picked.is_none() { let keyword = text.trim().to_ascii_uppercase();
return None; if self.operation == EdgeOperation::Chamfer {
return match self.step {
EdgeStep::Selecting => match keyword.as_str() {
"L" | "LOOP" => {
self.step = EdgeStep::PickingLoop;
Some(CmdResult::NeedPoint)
}
"D" | "DISTANCE" => Some(self.begin_distances(EdgeStep::Selecting)),
_ => None,
},
EdgeStep::PickingLoop => match keyword.as_str() {
"E" | "EDGE" => {
self.step = EdgeStep::Selecting;
Some(CmdResult::NeedPoint)
}
"D" | "DISTANCE" => Some(self.begin_distances(EdgeStep::PickingLoop)),
_ => None,
},
EdgeStep::LoopConfirm => match keyword.as_str() {
"A" | "ACCEPT" => Some(self.accept_loop()),
"N" | "NEXT" if !self.loop_candidates.is_empty() => {
self.loop_index = (self.loop_index + 1) % self.loop_candidates.len();
self.rebuild_preview();
Some(CmdResult::NeedPoint)
}
_ => None,
},
EdgeStep::PreviewConfirm => match keyword.as_str() {
"D" | "DISTANCE" => Some(self.begin_distances(EdgeStep::PreviewConfirm)),
_ => None,
},
EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 => {
if matches!(keyword.as_str(), "E" | "EXPRESSION") {
self.expression_return = self.step;
self.step = EdgeStep::Expression;
Some(CmdResult::NeedPoint)
} else {
let value = keyword.parse::<f64>().ok()?;
self.accept_distance(value)
}
}
EdgeStep::Expression => {
let value = crate::app::expr_eval::eval_number(text)?;
self.step = self.expression_return;
self.accept_distance(value)
}
EdgeStep::Radius => None,
};
}
match self.step {
EdgeStep::Selecting => match keyword.as_str() {
"R" | "RADIUS" => Some(self.begin_radius(EdgeStep::Selecting)),
"C" | "CHAIN" => {
self.selection_mode = EdgeSelectionMode::Chain;
Some(CmdResult::NeedPoint)
}
"E" | "EDGE" if self.selection_mode == EdgeSelectionMode::Chain => {
self.selection_mode = EdgeSelectionMode::Edge;
Some(CmdResult::NeedPoint)
}
"L" | "LOOP" => {
self.step = EdgeStep::PickingLoop;
Some(CmdResult::NeedPoint)
}
_ => None,
},
EdgeStep::PickingLoop => match keyword.as_str() {
"E" | "EDGE" => {
self.selection_mode = EdgeSelectionMode::Edge;
self.step = EdgeStep::Selecting;
Some(CmdResult::NeedPoint)
}
"C" | "CHAIN" => {
self.selection_mode = EdgeSelectionMode::Chain;
self.step = EdgeStep::Selecting;
Some(CmdResult::NeedPoint)
}
"R" | "RADIUS" => Some(self.begin_radius(EdgeStep::PickingLoop)),
_ => None,
},
EdgeStep::LoopConfirm => match keyword.as_str() {
"A" | "ACCEPT" => Some(self.accept_loop()),
"N" | "NEXT" if !self.loop_candidates.is_empty() => {
self.loop_index = (self.loop_index + 1) % self.loop_candidates.len();
self.rebuild_preview();
Some(CmdResult::NeedPoint)
}
_ => None,
},
EdgeStep::PreviewConfirm => match keyword.as_str() {
"R" | "RADIUS" => Some(self.begin_radius(EdgeStep::PreviewConfirm)),
_ => None,
},
EdgeStep::Radius => {
if matches!(keyword.as_str(), "E" | "EXPRESSION") {
self.step = EdgeStep::Expression;
return Some(CmdResult::NeedPoint);
}
let value = keyword.parse::<f64>().ok()?;
self.accept_radius(value)
}
EdgeStep::Expression => {
let value = crate::app::expr_eval::eval_number(text)?;
self.accept_radius(value)
}
EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 => None,
} }
let value = text.trim().parse::<f64>().ok()?;
(value > 0.0 && value.is_finite()).then(|| self.finish(value))
} }
fn on_enter(&mut self) -> CmdResult { fn on_enter(&mut self) -> CmdResult {
if self.picked.is_some() { match (self.operation, self.step) {
self.finish(self.default_value) (EdgeOperation::Chamfer, EdgeStep::Selecting) if !self.selected_edges.is_empty() => {
} else { self.step = EdgeStep::PreviewConfirm;
CmdResult::Cancel self.rebuild_preview();
CmdResult::NeedPoint
}
(EdgeOperation::Chamfer, EdgeStep::LoopConfirm) => self.accept_loop(),
(EdgeOperation::Chamfer, EdgeStep::PreviewConfirm) => self.finish(),
(EdgeOperation::Chamfer, EdgeStep::ChamferDistance1) => self
.accept_distance(self.default_value)
.unwrap_or(CmdResult::NeedPoint),
(EdgeOperation::Chamfer, EdgeStep::ChamferDistance2) => self
.accept_distance(self.other_value)
.unwrap_or(CmdResult::NeedPoint),
(EdgeOperation::Chamfer, EdgeStep::Expression) => CmdResult::NeedPoint,
(EdgeOperation::Chamfer, _) => CmdResult::Cancel,
(_, EdgeStep::Selecting) if !self.selected_edges.is_empty() => {
self.step = EdgeStep::PreviewConfirm;
self.rebuild_preview();
CmdResult::NeedPoint
}
(_, EdgeStep::LoopConfirm) => self.accept_loop(),
(_, EdgeStep::PreviewConfirm) => self.finish(),
(_, EdgeStep::Radius) => self
.accept_radius(self.default_value)
.unwrap_or(CmdResult::NeedPoint),
(_, EdgeStep::Expression) => CmdResult::NeedPoint,
_ => CmdResult::Cancel,
} }
} }
fn on_undo_step(&mut self) -> Option<CmdResult> {
if self.step == EdgeStep::LoopConfirm {
self.loop_candidates.clear();
self.loop_index = 0;
self.step = EdgeStep::Selecting;
self.rebuild_preview();
return Some(CmdResult::NeedPoint);
}
let (batch, _) = self.selection_batches.pop()?;
for edge in batch {
if let Some(index) = self.selected_edges.iter().position(|candidate| *candidate == edge) {
self.selected_edges.remove(index);
}
}
if self.selected_edges.is_empty() {
self.handle = None;
self.base_face = None;
}
self.last_pick = self
.selection_batches
.last()
.map(|(_, anchor)| *anchor);
self.step = EdgeStep::Selecting;
self.rebuild_preview();
Some(CmdResult::NeedPoint)
}
fn wants_text_input(&self) -> bool { fn wants_text_input(&self) -> bool {
self.picked.is_some() matches!(
self.step,
EdgeStep::Radius
| EdgeStep::Expression
| EdgeStep::ChamferDistance1
| EdgeStep::ChamferDistance2
)
} }
fn dyn_commit_as_text(&self) -> bool { fn dyn_commit_as_text(&self) -> bool {
self.picked.is_some() matches!(
self.step,
EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2
)
} }
fn dyn_spec(&self) -> Option<DynSpec> { fn dyn_spec(&self) -> Option<DynSpec> {
let (_, pick) = self.picked?; let pick = self.last_pick?;
Some(DynSpec { matches!(
self.step,
EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2
)
.then(|| DynSpec {
anchor: DynAnchor::Point(pick), anchor: DynAnchor::Point(pick),
fields: vec![DynFieldSpec::new(match self.operation { fields: vec![DynFieldSpec::new(match self.operation {
EdgeOperation::Fillet => DynRole::Radius, EdgeOperation::Fillet => DynRole::Radius,
@@ -137,10 +722,163 @@ impl CadCommand for SolidEdgeCommand {
} }
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> { fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
self.picked.map(|(_, pick)| cursor.distance(pick)) matches!(
self.step,
EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2
)
.then(|| self.last_pick.map(|pick| cursor.distance(pick)))
.flatten()
}
fn on_preview_wires(&mut self, cursor: DVec3) -> Vec<WireModel> {
if matches!(
self.step,
EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2
) {
if let Some(value) = self.last_pick.map(|pick| cursor.distance(pick)) {
if value > 0.0 && value.is_finite() {
let values = match self.step {
EdgeStep::ChamferDistance2 => (self.default_value, value),
_ => (value, self.other_value),
};
if let Some((handle, wires)) = self.preview_for_values(values.0, values.1) {
self.preview_wires = wires;
self.preview_hidden.clear();
self.preview_hidden.push(handle);
} else {
self.preview_wires.clear();
self.preview_hidden.clear();
}
}
}
}
self.preview_wires.clone()
}
fn preview_hidden_handles(&self) -> &[Handle] {
&self.preview_hidden
} }
} }
fn edge_loops(body: &Body, seed: EdgeKey) -> Vec<(FaceKey, Vec<EdgeKey>)> {
let Some(edge) = body.edges.get(seed) else {
return Vec::new();
};
let mut candidates = Vec::new();
for coedge_key in &edge.coedges {
let Some(coedge) = body.coedges.get(*coedge_key) else {
continue;
};
let Some(edge_loop) = body.loops.get(coedge.owner) else {
continue;
};
let edges = edge_loop
.coedges
.iter()
.filter_map(|key| body.coedges.get(*key).map(|coedge| coedge.edge))
.collect::<Vec<_>>();
let candidate = (edge_loop.owner, edges);
if !candidate.1.is_empty() && !candidates.contains(&candidate) {
candidates.push(candidate);
}
}
candidates
}
fn edge_belongs_to_face(body: &Body, edge: EdgeKey, face: FaceKey) -> bool {
body.edges.get(edge).is_some_and(|edge| {
edge.coedges.iter().any(|coedge| {
body.coedges
.get(*coedge)
.and_then(|coedge| body.loops.get(coedge.owner))
.is_some_and(|edge_loop| edge_loop.owner == face)
})
})
}
fn nearest_edge_face(body: &Body, edge: EdgeKey, point: [f64; 3]) -> Option<FaceKey> {
let edge = body.edges.get(edge)?;
edge.coedges
.iter()
.filter_map(|coedge| {
let edge_loop = body.loops.get(body.coedges.get(*coedge)?.owner)?;
let face = body.faces.get(edge_loop.owner)?;
let surface = body.surfaces.get(face.surface)?;
let distance = surface.distance_to(point).abs();
distance.is_finite().then_some((edge_loop.owner, distance))
})
.min_by(|first, second| first.1.total_cmp(&second.1))
.map(|(face, _)| face)
}
fn positive_default(value: f64) -> f64 {
if value.is_finite() && value > 0.0 {
value
} else {
1.0
}
}
fn edge_chain(body: &Body, seed: EdgeKey) -> Vec<EdgeKey> {
let mut chain = vec![seed];
let mut next = 0;
while next < chain.len() {
let current_key = chain[next];
next += 1;
let Some(current) = body.edges.get(current_key) else {
continue;
};
for vertex in [current.start, current.end] {
let Some(current_tangent) = edge_tangent_from_vertex(body, current_key, vertex) else {
continue;
};
for candidate_key in body.edge_keys() {
if chain.contains(&candidate_key) {
continue;
}
let Some(candidate) = body.edges.get(candidate_key) else {
continue;
};
if candidate.start != vertex && candidate.end != vertex {
continue;
}
let Some(candidate_tangent) =
edge_tangent_from_vertex(body, candidate_key, vertex)
else {
continue;
};
if current_tangent.dot(candidate_tangent) < -0.999 {
chain.push(candidate_key);
}
}
}
}
chain
}
fn edge_tangent_from_vertex(
body: &Body,
edge_key: EdgeKey,
vertex: cadkernel::brep::VertexKey,
) -> Option<DVec3> {
let edge = body.edges.get(edge_key)?;
let curve = body.curves.get(edge.curve)?;
let span = edge.end_parameter - edge.start_parameter;
if !span.is_finite() || span.abs() <= f64::EPSILON {
return None;
}
let step = span * 1.0e-6;
let (at, inside) = if edge.start == vertex {
(edge.start_parameter, edge.start_parameter + step)
} else if edge.end == vertex {
(edge.end_parameter, edge.end_parameter - step)
} else {
return None;
};
let tangent = DVec3::from(curve.point_at(inside)) - DVec3::from(curve.point_at(at));
(tangent.length_squared() > 0.0 && tangent.is_finite()).then(|| tangent.normalize())
}
inventory::submit!(crate::command::CommandRegistration { inventory::submit!(crate::command::CommandRegistration {
names: &["SOLIDFILLET", "SOLIDCHAMFER"] names: &["FILLETEDGE", "SOLIDFILLET", "CHAMFEREDGE", "SOLIDCHAMFER"]
}); });
+5 -2
View File
@@ -5,6 +5,8 @@ pub mod cylinder_cmd;
pub mod edge_cmd; pub mod edge_cmd;
pub mod polysolid_cmd; pub mod polysolid_cmd;
pub mod primitive_cmd; pub mod primitive_cmd;
pub mod shell_cmd;
pub mod slice_cmd;
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef}; use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
@@ -88,8 +90,9 @@ impl CadModule for ModelModule {
RibbonGroup { RibbonGroup {
title: "Edges", title: "Edges",
tools: vec![ tools: vec![
RibbonItem::LargeTool(tool("SOLIDFILLET", "Fillet", FILLET_ICON)), RibbonItem::LargeTool(tool("FILLETEDGE", "Fillet Edge", FILLET_ICON)),
RibbonItem::LargeTool(tool("SOLIDCHAMFER", "Chamfer", CHAMFER_ICON)), RibbonItem::LargeTool(tool("CHAMFEREDGE", "Chamfer", CHAMFER_ICON)),
RibbonItem::LargeTool(tool("SHELL", "Shell", PRESSPULL_ICON)),
], ],
}, },
] ]
+308
View File
@@ -0,0 +1,308 @@
use acadrust::Handle;
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, ShellFaceAction};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ShellStep {
SolidEditOption,
BodyOption,
SelectSolid,
RemoveFaces,
AddFaces,
Distance,
}
pub struct ShellCommand {
entry_name: &'static str,
step: ShellStep,
target: Option<Handle>,
actions: Vec<ShellFaceAction>,
default_distance: f64,
}
impl ShellCommand {
pub fn direct(target: Option<Handle>) -> Self {
Self {
entry_name: "SHELL",
step: if target.is_some() {
ShellStep::RemoveFaces
} else {
ShellStep::SelectSolid
},
target,
actions: Vec::new(),
default_distance: 1.0,
}
}
pub fn solid_edit(target: Option<Handle>) -> Self {
Self {
entry_name: "SOLIDEDIT",
step: ShellStep::SolidEditOption,
target,
actions: Vec::new(),
default_distance: 1.0,
}
}
fn finish(&mut self, distance: f64) -> CmdResult {
let Some(handle) = self.target else {
return CmdResult::Cancel;
};
CmdResult::SolidShell {
handle,
actions: std::mem::take(&mut self.actions),
distance,
}
}
fn undo_face_action(&mut self) -> CmdResult {
self.actions.pop();
CmdResult::NeedPoint
}
fn face_options(add: bool) -> Vec<CmdOption> {
if add {
vec![
CmdOption::new("Undo", "UNDO"),
CmdOption::new("Remove", "REMOVE"),
CmdOption::new("ALL", "ALL"),
]
} else {
vec![
CmdOption::new("Undo", "UNDO"),
CmdOption::new("Add", "ADD"),
CmdOption::new("ALL", "ALL"),
]
}
}
}
impl CadCommand for ShellCommand {
fn name(&self) -> &'static str {
self.entry_name
}
fn prompt(&self) -> String {
match self.step {
ShellStep::SolidEditOption => {
crate::t!("Enter a solid editing option [Body/eXit]:").into_owned()
}
ShellStep::BodyOption => {
crate::t!("Enter a body editing option [Shell/eXit]:").into_owned()
}
ShellStep::SelectSolid => crate::t!("Select a 3D solid:").into_owned(),
ShellStep::RemoveFaces => {
crate::t!("Select faces to remove [Undo/Add/ALL] <done>:").into_owned()
}
ShellStep::AddFaces => {
crate::t!("Select faces to add [Undo/Remove/ALL] <done>:").into_owned()
}
ShellStep::Distance => crate::tf!(
"Specify the shell offset distance <{:.3}>:",
self.default_distance
)
.into_owned(),
}
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
ShellStep::SolidEditOption => vec![
CmdOption::new("Body", "BODY"),
CmdOption::new("eXit", "EXIT"),
],
ShellStep::BodyOption => vec![
CmdOption::new("Shell", "SHELL"),
CmdOption::new("eXit", "EXIT"),
],
ShellStep::RemoveFaces => Self::face_options(false),
ShellStep::AddFaces => Self::face_options(true),
_ => Vec::new(),
}
}
fn needs_entity_pick(&self) -> bool {
matches!(
self.step,
ShellStep::SelectSolid | ShellStep::RemoveFaces | ShellStep::AddFaces
)
}
fn entity_pick_includes_fills(&self) -> bool {
true
}
fn entity_pick_uses_surface_point(&self) -> bool {
true
}
fn entity_pick_highlights_hover(&self) -> bool {
true
}
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
match self.step {
ShellStep::SelectSolid => {
self.target = Some(handle);
self.step = ShellStep::RemoveFaces;
}
ShellStep::RemoveFaces if self.target == Some(handle) => {
self.actions.push(ShellFaceAction::Remove(point));
}
ShellStep::AddFaces if self.target == Some(handle) => {
self.actions.push(ShellFaceAction::Add(point));
}
_ => {}
}
CmdResult::NeedPoint
}
fn on_point(&mut self, _point: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let value = text.trim();
let keyword = value.to_ascii_uppercase();
let result = match self.step {
ShellStep::SolidEditOption => match keyword.as_str() {
"B" | "BODY" => {
self.step = ShellStep::BodyOption;
CmdResult::NeedPoint
}
"X" | "EXIT" => CmdResult::Cancel,
_ => return Some(CmdResult::NeedPoint),
},
ShellStep::BodyOption => match keyword.as_str() {
"S" | "SHELL" => {
self.step = if self.target.is_some() {
ShellStep::RemoveFaces
} else {
ShellStep::SelectSolid
};
CmdResult::NeedPoint
}
"X" | "EXIT" => CmdResult::Cancel,
_ => return Some(CmdResult::NeedPoint),
},
ShellStep::RemoveFaces => match keyword.as_str() {
"U" | "UNDO" => return Some(self.undo_face_action()),
"A" | "ADD" => {
self.step = ShellStep::AddFaces;
CmdResult::NeedPoint
}
"ALL" => {
self.actions.push(ShellFaceAction::RemoveAll);
CmdResult::NeedPoint
}
_ => return Some(CmdResult::NeedPoint),
},
ShellStep::AddFaces => match keyword.as_str() {
"U" | "UNDO" => return Some(self.undo_face_action()),
"R" | "REMOVE" => {
self.step = ShellStep::RemoveFaces;
CmdResult::NeedPoint
}
"ALL" => {
self.actions.push(ShellFaceAction::AddAll);
CmdResult::NeedPoint
}
_ => return Some(CmdResult::NeedPoint),
},
ShellStep::Distance => {
let Ok(distance) = value.parse::<f64>() else {
return Some(CmdResult::ReportError(
crate::t!("A numeric shell offset distance is required.").into_owned(),
));
};
if !distance.is_finite() || distance.abs() <= f64::EPSILON {
return Some(CmdResult::ReportError(
crate::t!("Value must be nonzero.").into_owned(),
));
}
self.default_distance = distance;
self.finish(distance)
}
ShellStep::SelectSolid => return None,
};
Some(result)
}
fn on_enter(&mut self) -> CmdResult {
match self.step {
ShellStep::RemoveFaces | ShellStep::AddFaces => {
self.step = ShellStep::Distance;
CmdResult::NeedPoint
}
ShellStep::Distance => self.finish(self.default_distance),
_ => CmdResult::Cancel,
}
}
fn on_undo_step(&mut self) -> Option<CmdResult> {
matches!(self.step, ShellStep::RemoveFaces | ShellStep::AddFaces)
.then(|| self.undo_face_action())
}
fn input_kind(&self) -> InputKind {
if matches!(self.step, ShellStep::SelectSolid) {
InputKind::Point
} else {
InputKind::SingleToken
}
}
}
inventory::submit!(crate::command::CommandRegistration {
names: &["SHELL", "SOLIDEDIT"]
});
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn direct_shell_collects_face_edits_and_signed_distance() {
let handle = Handle::new(42);
let first = DVec3::new(1.0, 2.0, 3.0);
let second = DVec3::new(4.0, 5.0, 6.0);
let mut command = ShellCommand::direct(Some(handle));
command.on_entity_pick(handle, first);
command.on_text_input("ADD");
command.on_entity_pick(handle, second);
command.on_text_input("UNDO");
command.on_text_input("ALL");
assert!(matches!(command.on_enter(), CmdResult::NeedPoint));
assert!(matches!(
command.on_text_input("-0.5"),
Some(CmdResult::SolidShell {
handle: result_handle,
actions,
distance: -0.5,
}) if result_handle == handle
&& actions == vec![ShellFaceAction::Remove(first), ShellFaceAction::AddAll]
));
}
#[test]
fn invalid_distance_keeps_the_distance_step_active() {
let mut command = ShellCommand::direct(Some(Handle::new(7)));
command.on_enter();
assert!(matches!(
command.on_text_input("zero"),
Some(CmdResult::ReportError(_))
));
assert!(matches!(
command.on_text_input("0.25"),
Some(CmdResult::SolidShell { distance: 0.25, .. })
));
}
}
+510
View File
@@ -0,0 +1,510 @@
use acadrust::{EntityType, Handle};
use cadkernel::space::Plane;
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, SelectionEntity, WorkingPlane};
use crate::scene::model::wire_model::WireModel;
#[derive(Clone, Copy)]
enum PlaneKind {
Xy,
Yz,
Zx,
View,
}
enum Step {
Targets,
FirstPoint,
ThreeFirst,
ThreeSecond(DVec3),
ThirdPoint(DVec3, DVec3),
AxisFirst,
AxisSecond(DVec3),
PlanePoint(PlaneKind),
PickObject,
PickSurface,
Side(Plane),
SurfaceSide,
}
/// Interactive front-end for plane-based SLICE.
pub struct SliceCommand {
step: Step,
targets: Vec<Handle>,
selected: Vec<Handle>,
working: WorkingPlane,
view_normal: DVec3,
preview_center: DVec3,
preview_radius: f64,
picked: Option<EntityType>,
surface_tool: Option<cadkernel::brep::Body>,
notice: Option<&'static str>,
}
impl SliceCommand {
pub fn new(
targets: Vec<Handle>,
view_normal: DVec3,
preview_center: DVec3,
preview_radius: f64,
) -> Self {
let step = if targets.is_empty() { Step::Targets } else { Step::FirstPoint };
Self {
step,
targets,
selected: Vec::new(),
working: WorkingPlane::default(),
view_normal: view_normal.normalize_or(DVec3::Z),
preview_center,
preview_radius: preview_radius.max(1.0),
picked: None,
surface_tool: None,
notice: None,
}
}
fn plane(origin: DVec3, x: DVec3, normal: DVec3) -> Option<Plane> {
Plane::orthonormal(origin.to_array(), x.to_array(), normal.to_array())
}
fn enter_side(&mut self, plane: Plane) -> CmdResult {
self.notice = None;
self.step = Step::Side(plane);
CmdResult::NeedPoint
}
fn finish(&mut self, keep_point: Option<DVec3>) -> CmdResult {
let Step::Side(plane) = self.step else {
return CmdResult::NeedPoint;
};
CmdResult::SliceEntities {
targets: std::mem::take(&mut self.targets),
plane,
keep_point,
}
}
fn picked_plane(&self) -> Option<Plane> {
let entity = self.picked.as_ref()?;
if !matches!(
entity,
EntityType::Arc(_)
| EntityType::Circle(_)
| EntityType::Ellipse(_)
| EntityType::Spline(_)
| EntityType::LwPolyline(_)
| EntityType::Polyline(_)
) {
return None;
}
crate::scene::model::presspull_model::profile_geometry(entity)
.map(|(plane, _, _)| plane)
}
fn finish_surface(&mut self, keep_point: Option<DVec3>) -> CmdResult {
let Some(cutter) = self.surface_tool.take() else {
return CmdResult::NeedPoint;
};
CmdResult::SliceSurfaceEntities {
targets: std::mem::take(&mut self.targets),
cutter: Box::new(cutter),
keep_point,
}
}
fn plane_at(&self, point: DVec3, kind: PlaneKind) -> Option<Plane> {
let (x, normal) = match kind {
PlaneKind::Xy => (self.working.x, self.working.z),
PlaneKind::Yz => (self.working.y, self.working.x),
PlaneKind::Zx => (self.working.z, self.working.y),
PlaneKind::View => {
let candidate = if self.working.x.dot(self.view_normal).abs() < 0.98 {
self.working.x
} else {
self.working.y
};
(candidate, self.view_normal)
}
};
Self::plane(point, x, normal)
}
}
impl CadCommand for SliceCommand {
fn name(&self) -> &'static str {
"SLICE"
}
fn prompt(&self) -> String {
if let Some(notice) = self.notice {
return crate::t!(notice).into_owned();
}
match self.step {
Step::Targets => crate::t!(
"SLICE Select solids or surfaces to slice, then press Enter:"
)
.into_owned(),
Step::FirstPoint => crate::t!(
"SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:"
)
.into_owned(),
Step::ThreeFirst => {
crate::t!("SLICE Specify first point on slicing plane:").into_owned()
}
Step::ThreeSecond(_) => {
crate::t!("SLICE Specify second point on slicing plane:").into_owned()
}
Step::ThirdPoint(_, _) => {
crate::t!("SLICE Specify third point on slicing plane:").into_owned()
}
Step::AxisFirst => {
crate::t!("SLICE Specify a point on the slicing plane:").into_owned()
}
Step::AxisSecond(_) => {
crate::t!("SLICE Specify a point on the plane's normal axis:").into_owned()
}
Step::PlanePoint(PlaneKind::View) => {
crate::t!("SLICE Specify a point on the current view plane:").into_owned()
}
Step::PlanePoint(_) => {
crate::t!("SLICE Specify a point on the current UCS plane:").into_owned()
}
Step::PickObject => crate::t!("SLICE Select a planar curve object:").into_owned(),
Step::PickSurface => crate::t!("SLICE Select a cutting surface:").into_owned(),
Step::Side(_) => crate::t!(
"SLICE Specify a point on the desired side or [Both] <Both>:"
)
.into_owned(),
Step::SurfaceSide => crate::t!(
"SLICE Select a sliced object to keep or [Both] <Both>:"
)
.into_owned(),
}
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
Step::FirstPoint => vec![
CmdOption::new(crate::t!("planar Object").as_ref(), "O"),
CmdOption::new(crate::t!("Surface").as_ref(), "S"),
CmdOption::new(crate::t!("Zaxis").as_ref(), "Z"),
CmdOption::new(crate::t!("View").as_ref(), "V"),
CmdOption::new("XY", "XY"),
CmdOption::new("YZ", "YZ"),
CmdOption::new("ZX", "ZX"),
CmdOption::new(crate::t!("3points").as_ref(), "3"),
],
Step::Side(_) | Step::SurfaceSide => {
vec![CmdOption::new(crate::t!("Both").as_ref(), "B")]
}
_ => Vec::new(),
}
}
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.working = plane;
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
self.notice = None;
match self.step {
Step::FirstPoint => {
self.step = Step::ThreeSecond(point);
CmdResult::NeedPoint
}
Step::ThreeFirst => {
self.step = Step::ThreeSecond(point);
CmdResult::NeedPoint
}
Step::ThreeSecond(first) => {
self.step = Step::ThirdPoint(first, point);
CmdResult::NeedPoint
}
Step::ThirdPoint(first, second) => {
let x = second - first;
let normal = x.cross(point - first);
Self::plane(first, x, normal)
.map_or(CmdResult::NeedPoint, |plane| self.enter_side(plane))
}
Step::AxisFirst => {
self.step = Step::AxisSecond(point);
CmdResult::NeedPoint
}
Step::AxisSecond(origin) => {
let normal = point - origin;
let x = if self.working.x.dot(normal.normalize_or(DVec3::Z)).abs() < 0.98 {
self.working.x
} else {
self.working.y
};
Self::plane(origin, x, normal)
.map_or(CmdResult::NeedPoint, |plane| self.enter_side(plane))
}
Step::PlanePoint(kind) => self
.plane_at(point, kind)
.map_or(CmdResult::NeedPoint, |plane| self.enter_side(plane)),
Step::Side(_) => self.finish(Some(point)),
Step::SurfaceSide => self.finish_surface(Some(point)),
_ => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
match self.step {
Step::Targets if !self.selected.is_empty() => {
self.targets = std::mem::take(&mut self.selected);
self.step = Step::FirstPoint;
CmdResult::DeselectAndContinue
}
Step::FirstPoint => {
self.step = Step::ThreeFirst;
CmdResult::NeedPoint
}
Step::Side(_) => self.finish(None),
Step::SurfaceSide => self.finish_surface(None),
_ => CmdResult::NeedPoint,
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let keyword = text.trim().to_ascii_uppercase();
Some(match self.step {
Step::FirstPoint => match keyword.as_str() {
"O" | "OBJECT" => {
self.step = Step::PickObject;
CmdResult::NeedPoint
}
"S" | "SURFACE" => {
self.step = Step::PickSurface;
CmdResult::NeedPoint
}
"Z" | "ZAXIS" => {
self.step = Step::AxisFirst;
CmdResult::NeedPoint
}
"V" | "VIEW" => {
self.step = Step::PlanePoint(PlaneKind::View);
CmdResult::NeedPoint
}
"XY" => {
self.step = Step::PlanePoint(PlaneKind::Xy);
CmdResult::NeedPoint
}
"YZ" => {
self.step = Step::PlanePoint(PlaneKind::Yz);
CmdResult::NeedPoint
}
"ZX" | "XZ" => {
self.step = Step::PlanePoint(PlaneKind::Zx);
CmdResult::NeedPoint
}
"3" | "3P" | "3POINTS" => {
self.step = Step::ThreeFirst;
CmdResult::NeedPoint
}
_ => CmdResult::NeedPoint,
},
Step::Side(_) if matches!(keyword.as_str(), "" | "B" | "BOTH") => self.finish(None),
Step::SurfaceSide if matches!(keyword.as_str(), "" | "B" | "BOTH") => {
self.finish_surface(None)
}
_ => CmdResult::NeedPoint,
})
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::FirstPoint | Step::Side(_) | Step::SurfaceSide)
}
fn is_selection_gathering(&self) -> bool {
matches!(self.step, Step::Targets)
}
fn selection_forces_add(&self) -> bool {
true
}
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
self.selected = entities
.into_iter()
.filter_map(|item| {
matches!(item.entity, EntityType::Solid3D(_) | EntityType::Surface(_))
.then_some(item.handle)
})
.collect();
}
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
CmdResult::NeedPoint
}
fn needs_entity_pick(&self) -> bool {
matches!(
self.step,
Step::PickObject | Step::PickSurface | Step::SurfaceSide
)
}
fn entity_pick_highlights_hover(&self) -> bool {
self.needs_entity_pick()
}
fn inject_before_entity_pick(&self) -> bool {
matches!(self.step, Step::PickObject | Step::PickSurface)
}
fn entity_pick_uses_surface_point(&self) -> bool {
matches!(self.step, Step::SurfaceSide)
}
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked = Some(entity);
}
fn on_entity_pick(&mut self, _handle: Handle, _point: DVec3) -> CmdResult {
if matches!(self.step, Step::SurfaceSide) {
return self.finish_surface(Some(_point));
}
let surface_only = matches!(self.step, Step::PickSurface);
if surface_only {
let body = self.picked.as_ref().and_then(|entity| match entity {
EntityType::Surface(surface) => {
crate::scene::convert::solid3d_tess::kernel_surface_body(surface)
}
_ => None,
});
if let Some(body) = body {
if let Some(plane) = planar_body_plane(&body) {
return self.enter_side(plane);
}
if body.face_keys().count() == 1 {
self.surface_tool = Some(body);
self.notice = None;
self.step = Step::SurfaceSide;
return CmdResult::NeedPoint;
}
}
self.notice = Some(
"SLICE The selected surface does not define one supported cutting sheet. Select another surface:",
);
return CmdResult::NeedPoint;
}
match self.picked_plane() {
Some(plane) => self.enter_side(plane),
None => {
self.notice = Some(
"SLICE The selected object does not define a plane. Select a planar curve object:",
);
CmdResult::NeedPoint
}
}
}
fn on_preview_wires(&mut self, point: DVec3) -> Vec<WireModel> {
let mut wires = Vec::new();
let plane = match self.step {
Step::ThirdPoint(first, second) => {
wires.push(WireModel::solid_f64(
"slice_triangle".into(),
vec![
first.to_array(),
second.to_array(),
point.to_array(),
first.to_array(),
],
WireModel::CYAN,
false,
));
Self::plane(first, second - first, (second - first).cross(point - first))
}
Step::AxisSecond(origin) => {
wires.push(WireModel::solid_f64(
"slice_normal".into(),
vec![origin.to_array(), point.to_array()],
WireModel::CYAN,
false,
));
let normal = point - origin;
let x = if self.working.x.dot(normal.normalize_or(DVec3::Z)).abs() < 0.98 {
self.working.x
} else {
self.working.y
};
Self::plane(origin, x, normal)
}
Step::PlanePoint(kind) => self.plane_at(point, kind),
Step::Side(plane) => Some(plane),
_ => None,
};
if let Some(plane) = plane {
wires.extend(plane_grid(
plane,
self.preview_center,
self.preview_radius,
));
}
wires
}
}
fn planar_body_plane(body: &cadkernel::brep::Body) -> Option<Plane> {
let mut faces = body.face_keys();
let first = cadkernel::brep::planar_face_profile(body, faces.next()?)?.plane;
let normal = DVec3::from_array(first.normal()?);
let tolerance = cadkernel::brep::operation_tolerance(&[body]);
faces
.all(|face| {
let Some(profile) = cadkernel::brep::planar_face_profile(body, face) else {
return false;
};
profile.plane.normal().is_some_and(|other| {
normal.dot(DVec3::from_array(other)).abs() >= 1.0 - 1e-9
&& first
.distance_to(profile.plane.origin)
.is_some_and(|gap| gap.abs() <= tolerance * 4.0)
})
})
.then_some(first)
}
fn plane_grid(plane: Plane, centre: DVec3, radius: f64) -> Vec<WireModel> {
let normal = DVec3::from_array(plane.normal().unwrap_or([0.0, 0.0, 1.0]));
let projected = centre
- normal
* plane
.distance_to(centre.to_array())
.unwrap_or_default();
let x = DVec3::from_array(plane.x_axis);
let y = DVec3::from_array(plane.y_axis);
let color = [0.2, 0.55, 1.0, 1.0];
(-5..=5)
.flat_map(|index| {
let offset = radius * index as f64 / 5.0;
[
WireModel::solid_f64(
format!("slice_grid_x_{index}"),
vec![
(projected + x * offset - y * radius).to_array(),
(projected + x * offset + y * radius).to_array(),
],
color,
false,
),
WireModel::solid_f64(
format!("slice_grid_y_{index}"),
vec![
(projected + y * offset - x * radius).to_array(),
(projected + y * offset + x * radius).to_array(),
],
color,
false,
),
]
})
.collect()
}
inventory::submit!(crate::command::CommandRegistration {
names: &["SLICE", "SL"]
});
+84 -10
View File
@@ -84,6 +84,38 @@ fn entity_curves_on_plane(
} }
} }
fn entity_boundary_source_on_plane(
entity: &EntityType,
plane: WorkingPlane,
tolerance: f64,
) -> Option<BoundarySource> {
let curves = entity_curves_on_plane(entity, plane, tolerance);
if curves.is_empty() {
return None;
}
let segments: Vec<Line> = curves
.iter()
.flat_map(|curve| {
curve
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
.windows(2)
.filter_map(|pair| {
let start = pair[0];
let end = pair[1];
(start.iter().chain(&end).all(|value| value.is_finite())
&& start != end)
.then_some(Line { start, end })
})
.collect::<Vec<_>>()
})
.collect();
(!segments.is_empty()).then_some(BoundarySource { segments, curves })
}
fn hatch_path_seed(path: &acadrust::entities::BoundaryPath) -> Option<[f64; 2]> { fn hatch_path_seed(path: &acadrust::entities::BoundaryPath) -> Option<[f64; 2]> {
let mut ring = Vec::new(); let mut ring = Vec::new();
for edge in &path.edges { for edge in &path.edges {
@@ -731,10 +763,22 @@ impl Scene {
.boundary_handles .boundary_handles
.iter() .iter()
.filter_map(|source| { .filter_map(|source| {
all_sources // Associative boundaries are explicit document references.
.get(source) // During a grip drag their display wire can be preview-hidden,
.cloned() // so prefer the live entity geometry over the resident wire cache.
.map(|geometry| (*source, geometry)) let geometry = self
.document
.get_entity(*source)
.and_then(|entity| {
entity_boundary_source_on_plane(
entity,
plane,
WELD_TOLERANCE,
)
})
.or_else(|| all_sources.get(source).cloned());
geometry.map(|geometry| (*source, geometry))
}) })
.collect(); .collect();
let segments: Vec<_> = sources let segments: Vec<_> = sources
@@ -817,17 +861,47 @@ impl Scene {
.extend(segments); .extend(segments);
} }
for (&handle, source) in &mut sources { for (&handle, source) in &mut sources {
source.curves = self let curves = self
.document .document
.get_entity(handle) .get_entity(handle)
.map(|entity| entity_curves_on_plane(entity, plane, tolerance)) .map(|entity| entity_curves_on_plane(entity, plane, tolerance))
.unwrap_or_default(); .unwrap_or_default();
source.curves.retain(|curve| {
source.segments.iter().any(|segment| { // Associative hatch regeneration must use the entity's live geometry,
distance_to(curve, segment.start).max(distance_to(curve, segment.end)) // not a potentially one-frame-old display wire. Grip edits and STRETCH
<= tolerance * 4.0 // mutate the document before the resident wire cache catches up.
let live_segments: Vec<Line> = curves
.iter()
.flat_map(|curve| {
curve
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
.windows(2)
.filter_map(|pair| {
let start = pair[0];
let end = pair[1];
(start.iter().chain(&end).all(|value| value.is_finite())
&& start != end)
.then_some(Line { start, end })
})
.collect::<Vec<_>>()
}) })
}); .collect();
if !live_segments.is_empty() {
source.segments = live_segments;
source.curves = curves;
} else {
// Fallback for entity types that do not expose usable native curves:
// retain the existing display-wire geometry.
source.curves = curves;
source.curves.retain(|curve| {
source.segments.iter().any(|segment| {
distance_to(curve, segment.start).max(distance_to(curve, segment.end))
<= tolerance * 4.0
})
});
}
} }
sources sources
} }
+153 -9
View File
@@ -1108,7 +1108,7 @@ pub fn expand_insert(
); );
let mut first = first_batches.finalize(&name, selected, bg_color); let mut first = first_batches.finalize(&name, selected, bg_color);
for wire in &mut first { for wire in &mut first {
if wire.render_instance.is_none() { if !is_standalone_analytical_curve(wire) && wire.render_instance.is_none() {
wire.render_instance = Some( wire.render_instance = Some(
crate::scene::model::instance_model::RenderInstance { crate::scene::model::instance_model::RenderInstance {
source_id: crate::scene::model::instance_model::next_source_id(), source_id: crate::scene::model::instance_model::next_source_id(),
@@ -1149,7 +1149,7 @@ pub fn expand_insert(
if let Some(guard) = prototype_guard.as_mut() { if let Some(guard) = prototype_guard.as_mut() {
let translation = transform_translation(&xform); let translation = transform_translation(&xform);
for wire in &mut wires { for wire in &mut wires {
if wire.render_instance.is_none() { if !is_standalone_analytical_curve(wire) && wire.render_instance.is_none() {
wire.render_instance = Some(crate::scene::model::instance_model::RenderInstance { wire.render_instance = Some(crate::scene::model::instance_model::RenderInstance {
source_id: crate::scene::model::instance_model::next_source_id(), source_id: crate::scene::model::instance_model::next_source_id(),
translation, translation,
@@ -1314,6 +1314,20 @@ fn aabb_pixel_size(local_aabb: [f32; 4], world_per_pixel: f32) -> f32 {
w.max(h) / world_per_pixel w.max(h) / world_per_pixel
} }
fn is_standalone_analytical_curve(wire: &WireModel) -> bool {
wire.tangent_geoms.len() == 1
&& wire.fill_tris.is_empty()
&& wire.pick_tris.is_empty()
&& wire.text_verts.is_empty()
&& matches!(
wire.tangent_geoms[0],
TangentGeom::Circle { .. }
| TangentGeom::PlanarCircle { .. }
| TangentGeom::Arc { .. }
| TangentGeom::PlanarEllipse { .. }
)
}
struct ExpandCtx<'a> { struct ExpandCtx<'a> {
cache: &'a BlockCache, cache: &'a BlockCache,
ins_color: [f32; 4], ins_color: [f32; 4],
@@ -1931,12 +1945,14 @@ fn expand_defn(
.collect(); .collect();
crate::scene::pick::xclip::clip_wires(&mut wires, &world_poly); crate::scene::pick::xclip::clip_wires(&mut wires, &world_poly);
for wire in &mut wires { for wire in &mut wires {
wire.render_instance = Some( if !is_standalone_analytical_curve(wire) {
crate::scene::model::instance_model::RenderInstance { wire.render_instance = Some(
source_id: crate::scene::model::instance_model::next_source_id(), crate::scene::model::instance_model::RenderInstance {
translation, source_id: crate::scene::model::instance_model::next_source_id(),
}, translation,
); },
);
}
} }
out.extra_wires.extend(wires.iter().cloned()); out.extra_wires.extend(wires.iter().cloned());
first = Some((wires, translation)); first = Some((wires, translation));
@@ -1984,7 +2000,7 @@ fn expand_defn(
); );
let mut wires = sub.finalize("", ctx.selected, ctx.bg_color); let mut wires = sub.finalize("", ctx.selected, ctx.bg_color);
for wire in &mut wires { for wire in &mut wires {
if wire.render_instance.is_none() { if !is_standalone_analytical_curve(wire) && wire.render_instance.is_none() {
wire.render_instance = Some( wire.render_instance = Some(
crate::scene::model::instance_model::RenderInstance { crate::scene::model::instance_model::RenderInstance {
source_id: crate::scene::model::instance_model::next_source_id(), source_id: crate::scene::model::instance_model::next_source_id(),
@@ -2297,6 +2313,134 @@ fn emit_wire(
lw.line_weight_px lw.line_weight_px
}; };
// Analytical GPU circle / arc / ellipse fast path:
// If this LocalWire is a pure analytical curve (single tangent geometry, no fills/text/pick),
// and transforms cleanly with accum_xform into an analytical curve, emit it as a standalone
// wire in extra_wires. This lets partition_wires extract it into CircleGpu / EllipseGpu for
// pixel-perfect analytical rendering on the GPU instead of segmented lines.
if !lw.tangent_geoms.is_empty()
&& lw.fill_tris.is_empty()
&& lw.pick_tris.is_empty()
&& lw.text_verts.is_empty()
{
let transformed_tangents: Option<Vec<TangentGeom>> = lw
.tangent_geoms
.iter()
.map(|tg| transform_tangent(tg, accum_xform))
.collect();
if let Some(tangents) = transformed_tangents {
if tangents.iter().all(|tangent| {
matches!(
tangent,
TangentGeom::Circle { .. }
| TangentGeom::PlanarCircle { .. }
| TangentGeom::Arc { .. }
| TangentGeom::PlanarEllipse { .. }
)
}) {
let contrast_bg = lw.contrast_bg.unwrap_or(ctx.bg_color);
let color = if lw.canvas_color {
ctx.bg_color
} else if lw.preserve_color {
final_color
} else {
crate::scene::view::render::adapt_to_bg(final_color, contrast_bg)
};
let bg_adapt: crate::scene::model::wire_model::BgAdapt =
(lw.canvas_color || !lw.preserve_color).then(|| {
Box::new(crate::scene::model::wire_model::BgAdaptInputs {
raw_color: final_color,
text_raw_colors: Vec::new(),
contrast_bg: lw.contrast_bg,
canvas_color: lw.canvas_color,
preserve_color: lw.preserve_color,
})
});
let mut points = Vec::with_capacity(lw.points.len());
let mut points_low = Vec::with_capacity(lw.points.len());
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut max_y = f32::NEG_INFINITY;
for (idx, p) in lw.points.iter().enumerate() {
let pl = lw.points_low.get(idx).copied().unwrap_or([0.0; 3]);
let point = accum_xform.apply(Vector3::new(
p[0] as f64 + pl[0] as f64,
p[1] as f64 + pl[1] as f64,
p[2] as f64 + pl[2] as f64,
));
let (hx, lx) = WireModel::split_ds(point.x);
let (hy, ly) = WireModel::split_ds(point.y);
let (hz, lz) = WireModel::split_ds(point.z);
if hx < min_x { min_x = hx; }
if hy < min_y { min_y = hy; }
if hx > max_x { max_x = hx; }
if hy > max_y { max_y = hy; }
points.push([hx, hy, hz]);
points_low.push([lx, ly, lz]);
}
let aabb = if min_x.is_infinite() {
WireModel::UNBOUNDED_AABB
} else {
[min_x, min_y, max_x, max_y]
};
let mut snap_pts = Vec::with_capacity(lw.snap_pts.len());
for (p, hint) in &lw.snap_pts {
let v = accum_xform.apply(Vector3::new(p.x, p.y, p.z));
snap_pts.push((glam::DVec3::new(v.x, v.y, v.z), *hint));
}
let mut key_vertices = Vec::with_capacity(lw.key_vertices.len());
for p in &lw.key_vertices {
let v = accum_xform.apply(Vector3::new(p[0], p[1], p[2]));
key_vertices.push([v.x, v.y, v.z]);
}
let local_depth = if d_range != (0.0, 1.0) {
Some(d_range.0)
} else {
None
};
let wire = WireModel {
bg_adapt,
point_marker: lw.point_marker,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: local_depth,
display_visible: !lw.hide_unselected || ctx.selected,
plot_visible: lw.plot_visible,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name: String::new(),
points,
points_low,
color,
selected: ctx.selected,
pattern_length: final_pat_len,
pattern: final_pat,
line_weight_px: final_lw_px,
aci: final_aci,
snap_pts,
tangent_geoms: tangents,
key_vertices,
aabb,
plinegen: lw.plinegen,
fill_tris: Vec::new(),
fill_tris_low: Vec::new(),
};
out.extra_wires.push(wire);
return;
}
}
}
let station_values = pattern_station_values(&lw.pattern_stations, lw.points.len()); let station_values = pattern_station_values(&lw.pattern_stations, lw.points.len());
let station_map = decode_pattern_station_map(&lw.pattern_stations, lw.points.len()); let station_map = decode_pattern_station_map(&lw.pattern_stations, lw.points.len());
let transformed_stations = station_values.and_then(|(values, _)| { let transformed_stations = station_values.and_then(|(values, _)| {
+51
View File
@@ -21,3 +21,54 @@ pub fn solid_to_sat(body: &Body) -> Option<SatDocument> {
let binary = SabWriter::write(&document); let binary = SabWriter::write(&document);
valid(&SabReader::read(&binary).ok()?).then_some(document) valid(&SabReader::read(&binary).ok()?).then_some(document)
} }
#[cfg(test)]
mod tests {
use super::*;
use cadkernel::geom2d::{Arc, Curve};
use cadkernel::space::Plane;
fn circle_section(z: f64, radius: f64) -> (Plane, Vec<Curve>) {
let curves = (0..4)
.map(|part| {
let start = std::f64::consts::FRAC_PI_2 * part as f64;
Curve::Arc(Arc {
centre: [0.0, 0.0],
radius,
start_angle: start,
end_angle: start + std::f64::consts::FRAC_PI_2,
})
})
.collect();
(
Plane::from_axes([0.0, 0.0, z], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
curves,
)
}
#[test]
fn a_curved_loft_round_trips_through_text_and_binary_acis() {
let body = cadkernel::brep::loft(&[
circle_section(0.0, 5.0),
circle_section(10.0, 2.0),
])
.unwrap();
assert!(body.validate().is_empty());
let mut document = SatDocument::new();
cadkernel::acis::append(&body, &mut document).unwrap();
let text = document.to_sat_string();
let parsed = SatDocument::parse(&text).unwrap();
let (restored, text_loss) = cadkernel::acis::lift(&parsed);
assert!(text_loss.is_empty(), "{text_loss:?}");
assert_eq!(restored.len(), 1);
assert!(restored[0].validate().is_empty());
let binary = SabWriter::write(&parsed);
let parsed_binary = SabReader::read(&binary).unwrap();
let (restored, binary_loss) = cadkernel::acis::lift(&parsed_binary);
assert!(binary_loss.is_empty(), "{binary_loss:?}");
assert_eq!(restored.len(), 1);
assert!(restored[0].validate().is_empty());
}
}
+5
View File
@@ -177,6 +177,11 @@ pub fn tessellate_sat(
color, color,
None, None,
)); ));
if let [single] = bodies.as_slice() {
if let Some(properties) = brep::analytic_mass_properties(&single.0) {
set.apply_mass_properties(properties);
}
}
set.curved_gens = curved_gens; set.curved_gens = curved_gens;
for point in edges { for point in edges {
let high = [point[0] as f32, point[1] as f32, point[2] as f32]; let high = [point[0] as f32, point[1] as f32, point[2] as f32];
+4
View File
@@ -159,6 +159,10 @@ pub fn kernel_body(solid: &Solid3D) -> Option<KernelBody> {
kernel_acis_body(&solid.acis_data) kernel_acis_body(&solid.acis_data)
} }
pub fn kernel_region_body(region: &Region) -> Option<KernelBody> {
kernel_acis_body(&region.acis_data)
}
pub fn kernel_surface_body(surface: &Surface) -> Option<KernelBody> { pub fn kernel_surface_body(surface: &Surface) -> Option<KernelBody> {
kernel_acis_body(&surface.acis_data) kernel_acis_body(&surface.acis_data)
} }
+281 -36
View File
@@ -19,7 +19,7 @@ use acadrust::{CadDocument, EntityType, Handle};
use glam::Vec3; use glam::Vec3;
use crate::scene::convert::acad_to_render::{convert, RenderObject}; use crate::scene::convert::acad_to_render::{convert, RenderObject};
use crate::scene::model::wire_model::{SnapHint, WireModel}; use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel};
/// Split an f64 offset-relative coordinate into the double-single (high, low) /// Split an f64 offset-relative coordinate into the double-single (high, low)
/// f32 pair the renderer consumes. `high + low ≈ value` to ~f64 precision; the /// f32 pair the renderer consumes. `high + low ≈ value` to ~f64 precision; the
@@ -202,6 +202,176 @@ pub(crate) fn points_to_ds(
(high, low) (high, low)
} }
fn split_mixed_polyline(
tangent_geoms: &[TangentGeom],
key_vertices: &[[f64; 3]],
name: &str,
color: [f32; 4],
selected: bool,
pattern_length: f32,
pattern: [f32; 8],
line_weight_px: f32,
snap_pts: Vec<(glam::DVec3, SnapHint)>,
point_marker: Option<crate::scene::model::wire_model::PointMarker>,
plinegen: bool,
) -> Vec<WireModel> {
let mut out = Vec::new();
// Emit each arc segment as an analytical wire (CircleGpu target)
for tg in tangent_geoms {
if let TangentGeom::Arc {
center,
axis_x,
axis_y,
radius,
start_angle,
end_angle,
} = *tg
{
let mut arc_pts = Vec::with_capacity(17);
let n = 16;
let sweep = if end_angle >= start_angle {
end_angle - start_angle
} else {
end_angle - start_angle + std::f64::consts::TAU
};
for s in 0..=n {
let frac = s as f64 / n as f64;
let ang = start_angle + frac * sweep;
let p = [
center[0] + radius * (ang.cos() * axis_x[0] + ang.sin() * axis_y[0]),
center[1] + radius * (ang.cos() * axis_x[1] + ang.sin() * axis_y[1]),
center[2] + radius * (ang.cos() * axis_x[2] + ang.sin() * axis_y[2]),
];
arc_pts.push(p);
}
let (points, points_low) = points_to_ds(arc_pts);
out.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name: name.to_string(),
points,
points_low,
color,
selected,
pattern_length,
pattern,
line_weight_px,
snap_pts: Vec::new(),
tangent_geoms: vec![tg.clone()],
aci: 0,
key_vertices: Vec::new(),
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
}
}
// Collect straight lines into a line wire
let mut straight_pts: Vec<[f64; 3]> = Vec::new();
let mut straight_tangents: Vec<TangentGeom> = Vec::new();
let mut last_end: Option<[f64; 3]> = None;
for (i, tg) in tangent_geoms.iter().enumerate() {
if let TangentGeom::Line { p1, p2 } = tg {
straight_tangents.push(tg.clone());
let (p_start, p_end) = if i + 1 < key_vertices.len() {
(key_vertices[i], key_vertices[i + 1])
} else {
(
[p1[0] as f64, p1[1] as f64, p1[2] as f64],
[p2[0] as f64, p2[1] as f64, p2[2] as f64],
)
};
if !plinegen {
if !straight_pts.is_empty() {
straight_pts.push([f64::NAN; 3]);
}
straight_pts.push(p_start);
straight_pts.push(p_end);
} else {
if let Some(prev) = last_end {
if (prev[0] - p_start[0]).abs() < 1e-7
&& (prev[1] - p_start[1]).abs() < 1e-7
&& (prev[2] - p_start[2]).abs() < 1e-7
{
straight_pts.push(p_end);
} else {
straight_pts.push([f64::NAN; 3]);
straight_pts.push(p_start);
straight_pts.push(p_end);
}
} else {
straight_pts.push(p_start);
straight_pts.push(p_end);
}
last_end = Some(p_end);
}
} else {
last_end = None;
}
}
let (line_pts, line_pts_low) = points_to_ds(straight_pts);
if !line_pts.is_empty() {
out.push(WireModel {
bg_adapt: None,
point_marker,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name: name.to_string(),
points: line_pts,
points_low: line_pts_low,
color,
selected,
pattern_length,
pattern,
line_weight_px,
snap_pts,
tangent_geoms: straight_tangents,
aci: 0,
key_vertices: key_vertices.to_vec(),
aabb: WireModel::UNBOUNDED_AABB,
plinegen,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
} else if let Some(first_arc) = out.first_mut() {
first_arc.snap_pts = snap_pts;
first_arc.key_vertices = key_vertices.to_vec();
}
out
}
fn point_cloud_wires( fn point_cloud_wires(
document: &CadDocument, document: &CadDocument,
handle: Handle, handle: Handle,
@@ -707,9 +877,11 @@ pub fn tessellate(
} }
// ── Try the kernel path first ─────────────────────────────────────────── // ── Try the kernel path first ───────────────────────────────────────────
// Zoom-adaptive circles scale their segment count from the current zoom level and radius.
// Relative-PDSIZE points size their glyph from the current zoom so they // Relative-PDSIZE points size their glyph from the current zoom so they
// stay a roughly constant on-screen size; otherwise the header-driven path. // stay a roughly constant on-screen size; otherwise the header-driven path.
let te = crate::entities::point::relative_render(entity, document, world_per_pixel) let te = crate::entities::circle::relative_render(entity, document, world_per_pixel)
.or_else(|| crate::entities::point::relative_render(entity, document, world_per_pixel))
.or_else(|| crate::entities::light::relative_render(entity, document, world_per_pixel)) .or_else(|| crate::entities::light::relative_render(entity, document, world_per_pixel))
.or_else(|| match entity { .or_else(|| match entity {
EntityType::Text(text) => Some(crate::entities::text::to_render_at_scale( EntityType::Text(text) => Some(crate::entities::text::to_render_at_scale(
@@ -1567,40 +1739,71 @@ pub fn tessellate(
}; };
let point_marker = let point_marker =
crate::entities::point::relative_marker_spec(entity, document); crate::entities::point::relative_marker_spec(entity, document);
out.push(WireModel {
bg_adapt: None, let has_arc = te
point_marker, .tangent_geoms
taper_widths: Vec::new(), .iter()
pattern_stations: Vec::new(), .any(|tg| matches!(tg, TangentGeom::Arc { .. }));
world_width: polyline_band_width(entity, document.header.fill_mode), let has_line = te
depth_override: None, .tangent_geoms
display_visible: true, .iter()
plot_visible: true, .any(|tg| matches!(tg, TangentGeom::Line { .. }));
fill_is_3d, let is_thin_polyline = !is_thick_extrusion
fill_is_2d_solid: false, && polyline_band_width(entity, document.header.fill_mode) <= 1e-9
render_instance: None, && fill_tris.is_empty()
pick_tris, && pick_tris.is_empty()
pick_tris_low, && matches!(entity, EntityType::LwPolyline(_) | EntityType::Polyline2D(_));
dash_from_start: false,
dash_align_end: None, if has_arc && has_line && is_thin_polyline {
text_verts: Vec::new(), out.extend(split_mixed_polyline(
name: name.clone(), &te.tangent_geoms,
points: local_pts, &keys,
points_low: local_pts_low, &name,
color: edge_color, edge_color,
selected, selected,
pattern_length: edge_pattern_length, edge_pattern_length,
pattern: edge_pattern, edge_pattern,
line_weight_px, line_weight_px,
snap_pts: snap, snap,
tangent_geoms: tangents, point_marker,
aci: 0, true,
key_vertices: keys, ));
aabb: WireModel::UNBOUNDED_AABB, } else {
plinegen: true, out.push(WireModel {
fill_tris: vec![], bg_adapt: None,
fill_tris_low: Vec::new(), point_marker,
}); taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: polyline_band_width(entity, document.header.fill_mode),
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d,
fill_is_2d_solid: false,
render_instance: None,
pick_tris,
pick_tris_low,
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name: name.clone(),
points: local_pts,
points_low: local_pts_low,
color: edge_color,
selected,
pattern_length: edge_pattern_length,
pattern: edge_pattern,
line_weight_px,
snap_pts: snap,
tangent_geoms: tangents,
aci: 0,
key_vertices: keys,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
}
} }
if !fill_tris.is_empty() { if !fill_tris.is_empty() {
@@ -1756,6 +1959,48 @@ pub fn tessellate(
// A wide polyline with PLINEGEN=0 arrives here: same shader-band // A wide polyline with PLINEGEN=0 arrives here: same shader-band
// treatment as the Contour arm, restarting the dash per segment. // treatment as the Contour arm, restarting the dash per segment.
let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris); let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris);
let is_thick_extrusion = matches!(
entity,
EntityType::LwPolyline(p) if p.thickness.abs() > 1e-10
) || matches!(
entity,
EntityType::Polyline2D(p) if p.thickness.abs() > 1e-10
);
let is_thin_polyline = !is_thick_extrusion
&& polyline_band_width(entity, document.header.fill_mode) <= 1e-9
&& pick_tris.is_empty()
&& matches!(entity, EntityType::LwPolyline(_) | EntityType::Polyline2D(_));
let has_arc = te
.tangent_geoms
.iter()
.any(|tg| matches!(tg, TangentGeom::Arc { .. }));
let has_line = te
.tangent_geoms
.iter()
.any(|tg| matches!(tg, TangentGeom::Line { .. }));
if has_arc && has_line && is_thin_polyline {
let edge_color = if is_thick_extrusion {
[0.0, 0.0, 0.0, 1.0]
} else {
color
};
let point_marker =
crate::entities::point::relative_marker_spec(entity, document);
return split_mixed_polyline(
&te.tangent_geoms,
&key_vertices,
&name,
edge_color,
selected,
pattern_length,
pattern,
line_weight_px,
snap_pts,
point_marker,
false,
);
}
return vec![WireModel { return vec![WireModel {
bg_adapt: None, bg_adapt: None,
point_marker: None, point_marker: None,
+5 -2
View File
@@ -596,12 +596,15 @@ impl Scene {
.filter_map(|&handle| { .filter_map(|&handle| {
let from_history = self let from_history = self
.document .document
.solid_history_operation(handle) .solid_history_operations(handle)
.and_then(|operation| cadkernel::acis::rebuild_body(operation).ok()); .and_then(|operations| cadkernel::acis::rebuild_history(&operations).ok());
let body = from_history.or_else(|| match self.document.get_entity(handle) { let body = from_history.or_else(|| match self.document.get_entity(handle) {
Some(EntityType::Solid3D(solid)) => { Some(EntityType::Solid3D(solid)) => {
crate::scene::convert::solid3d_tess::kernel_body(solid) crate::scene::convert::solid3d_tess::kernel_body(solid)
} }
Some(EntityType::Region(region)) => {
crate::scene::convert::solid3d_tess::kernel_region_body(region)
}
Some(EntityType::Surface(surface)) => { Some(EntityType::Surface(surface)) => {
crate::scene::convert::solid3d_tess::kernel_surface_body(surface) crate::scene::convert::solid3d_tess::kernel_surface_body(surface)
} }
+888 -101
View File
File diff suppressed because it is too large. Load diff
+77 -61
View File
@@ -55,6 +55,44 @@ pub struct MeshMetrics {
pub surface_area: f64, pub surface_area: f64,
pub volume: f64, pub volume: f64,
pub centroid: [f64; 3], pub centroid: [f64; 3],
pub moment_of_inertia: [f64; 3],
pub principal_directions: [f64; 9],
pub principal_moments: [f64; 3],
pub product_of_inertia: [f64; 3],
pub radii_of_gyration: [f64; 3],
}
impl MeshMetrics {
fn apply_mass_properties(&mut self, properties: cadkernel::brep::MassProperties) {
self.volume = properties.volume;
self.centroid = properties.centroid;
self.moment_of_inertia = properties.moment_of_inertia;
self.principal_directions = properties.principal_directions;
self.principal_moments = properties.principal_moments;
self.product_of_inertia = properties.product_of_inertia;
self.radii_of_gyration = properties.radii_of_gyration;
}
pub fn translate(&mut self, delta: [f64; 3]) {
if self.volume > 1e-18 {
self.apply_mass_properties(
cadkernel::brep::MassProperties {
volume: self.volume,
centroid: self.centroid,
moment_of_inertia: self.moment_of_inertia,
principal_directions: self.principal_directions,
principal_moments: self.principal_moments,
product_of_inertia: self.product_of_inertia,
radii_of_gyration: self.radii_of_gyration,
}
.translated(delta),
);
} else {
for axis in 0..3 {
self.centroid[axis] += delta[axis];
}
}
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -146,74 +184,52 @@ fn compute_mesh_metrics(lods: &[MeshModel]) -> MeshMetrics {
let Some(mesh) = lods.iter().find(|mesh| !mesh.indices.is_empty()) else { let Some(mesh) = lods.iter().find(|mesh| !mesh.indices.is_empty()) else {
return MeshMetrics::default(); return MeshMetrics::default();
}; };
let point = |index: u32| { let kernel_mesh = cadkernel::brep::Mesh {
let index = index as usize; positions: mesh
let high = mesh.verts.get(index).copied().unwrap_or([0.0; 3]); .verts
let low = mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]); .iter()
[ .enumerate()
high[0] as f64 + low[0] as f64, .map(|(index, high)| {
high[1] as f64 + low[1] as f64, let low = mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]);
high[2] as f64 + low[2] as f64, [
] high[0] as f64 + low[0] as f64,
high[1] as f64 + low[1] as f64,
high[2] as f64 + low[2] as f64,
]
})
.collect(),
normals: Vec::new(),
triangles: mesh
.indices
.chunks_exact(3)
.map(|triangle| {
[
triangle[0] as usize,
triangle[1] as usize,
triangle[2] as usize,
]
})
.collect(),
}; };
let mut area = 0.0; let surface = kernel_mesh.surface_properties();
let mut area_centroid_numerator = [0.0; 3]; let mut metrics = MeshMetrics {
let mut signed_volume = 0.0;
let mut centroid_numerator = [0.0; 3];
for triangle in mesh.indices.chunks_exact(3) {
let a = point(triangle[0]);
let b = point(triangle[1]);
let c = point(triangle[2]);
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
let cross = [
ab[1] * ac[2] - ab[2] * ac[1],
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
];
let triangle_area = 0.5
* (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2])
.sqrt();
area += triangle_area;
for axis in 0..3 {
area_centroid_numerator[axis] +=
triangle_area * (a[axis] + b[axis] + c[axis]) / 3.0;
}
let tetra = (
a[0] * (b[1] * c[2] - b[2] * c[1])
- a[1] * (b[0] * c[2] - b[2] * c[0])
+ a[2] * (b[0] * c[1] - b[1] * c[0])
) / 6.0;
signed_volume += tetra;
for axis in 0..3 {
centroid_numerator[axis] += tetra * (a[axis] + b[axis] + c[axis]) / 4.0;
}
}
let centroid = if signed_volume.abs() > 1e-18 {
[
centroid_numerator[0] / signed_volume,
centroid_numerator[1] / signed_volume,
centroid_numerator[2] / signed_volume,
]
} else if area > 1e-18 {
[
area_centroid_numerator[0] / area,
area_centroid_numerator[1] / area,
area_centroid_numerator[2] / area,
]
} else {
[0.0; 3]
};
MeshMetrics {
vertices: mesh.verts.len(), vertices: mesh.verts.len(),
triangles: mesh.indices.len() / 3, triangles: mesh.indices.len() / 3,
surface_area: area, surface_area: surface.map_or(0.0, |value| value.0),
volume: signed_volume.abs(), centroid: surface.map_or([0.0; 3], |value| value.1),
centroid, ..MeshMetrics::default()
};
if let Some(properties) = kernel_mesh.inertial_properties() {
metrics.apply_mass_properties(properties);
} }
metrics
} }
impl MeshLodSet { impl MeshLodSet {
pub fn apply_mass_properties(&mut self, properties: cadkernel::brep::MassProperties) {
self.metrics.apply_mass_properties(properties);
}
/// Build a set from its LODs, computing the 3D AABB. /// Build a set from its LODs, computing the 3D AABB.
pub fn from_lods(lods: Vec<MeshModel>) -> Self { pub fn from_lods(lods: Vec<MeshModel>) -> Self {
let (world_aabb, z_aabb) = compute_mesh_aabb(&lods); let (world_aabb, z_aabb) = compute_mesh_aabb(&lods);
+336 -8
View File
@@ -1,12 +1,13 @@
use acadrust::entities::{EmbeddedEntity, Solid3D}; use acadrust::entities::{EmbeddedEntity, Solid3D};
use acadrust::objects::{ use acadrust::objects::{
DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone, DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryChamfer,
SolidHistoryCylinder, SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation, SolidHistoryCone, SolidHistoryCylinder, SolidHistoryFillet, SolidHistoryLoft,
SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation,
SolidHistoryPyramid, SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep, SolidHistoryPyramid, SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep,
SolidHistoryTorus, SolidHistoryTorus,
}; };
use acadrust::EntityType; use acadrust::EntityType;
use cadkernel::brep::Body; use cadkernel::brep::{Body, Surface};
use crate::command::EntityTransform; use crate::command::EntityTransform;
use crate::entities::traits::EntityTypeOps; use crate::entities::traits::EntityTypeOps;
@@ -32,6 +33,9 @@ pub const GRIP_SWEEP_PATH_FIRST: usize = 30_000;
pub const GRIP_LOFT_SECTION_FIRST: usize = 40_000; pub const GRIP_LOFT_SECTION_FIRST: usize = 40_000;
pub const GRIP_REVOLVE_PROFILE_FIRST: usize = 50_000; pub const GRIP_REVOLVE_PROFILE_FIRST: usize = 50_000;
pub const GRIP_REVOLVE_AXIS: usize = 10_013; pub const GRIP_REVOLVE_AXIS: usize = 10_013;
pub const GRIP_FILLET_RADIUS: usize = 10_014;
pub const GRIP_CHAMFER_DISTANCE1: usize = 10_015;
pub const GRIP_CHAMFER_DISTANCE2: usize = 10_016;
pub const GRIP_BOX_CORNER_FIRST: usize = 10_100; pub const GRIP_BOX_CORNER_FIRST: usize = 10_100;
pub const GRIP_BOX_FACE_X_MIN: usize = 10_110; pub const GRIP_BOX_FACE_X_MIN: usize = 10_110;
pub const GRIP_BOX_FACE_X_MAX: usize = 10_111; pub const GRIP_BOX_FACE_X_MAX: usize = 10_111;
@@ -197,7 +201,7 @@ pub fn has_specialized_primitive_properties(
handle: acadrust::Handle, handle: acadrust::Handle,
) -> bool { ) -> bool {
matches!( matches!(
document.solid_history_operation(handle), primitive_property_operation(document, handle).as_ref(),
Some( Some(
SolidHistoryOperation::Box(_) SolidHistoryOperation::Box(_)
| SolidHistoryOperation::Wedge(_) | SolidHistoryOperation::Wedge(_)
@@ -214,6 +218,32 @@ pub fn has_specialized_primitive_properties(
) )
} }
/// Return the primitive that owns the public Geometry rows. Later edge
/// operations refine that primitive without replacing its editable type,
/// position, or dimensions.
pub fn primitive_property_operation(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
) -> Option<SolidHistoryOperation> {
document
.solid_history_operations(handle)
.and_then(|operations| operations.into_iter().next())
.or_else(|| document.solid_history_operation(handle).cloned())
}
/// Solid history results whose public Properties palette is fully described by
/// the common entity rows plus [`primitive_properties`]. A generic B-rep has
/// no stable primitive dimensions to expose, but it still uses the compact
/// Solid History palette rather than the internal ACIS/cache diagnostics.
pub fn has_compact_solid_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
) -> bool {
document
.get_entity(handle)
.is_some_and(|entity| matches!(entity, acadrust::EntityType::Solid3D(_)))
}
pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3> { pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3> {
match operation { match operation {
SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => world_point( SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => world_point(
@@ -975,6 +1005,77 @@ fn cone_properties(
] ]
} }
fn brep_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
) -> Vec<PropSection> {
let (record_history, object_show_history, show_history_mode) =
history_flags(document, handle).unwrap_or((false, false, 1));
let (show_history, show_history_editable) =
displayed_history_state(object_show_history, show_history_mode);
let mut geometry = vec![Property {
label: t!("Solid type").into_owned(),
field: "solid_history_type",
value: PropValue::ReadOnly(t!("3D Solid").into_owned()),
}];
if let Some(position) = document
.solid_history_operation(handle)
.and_then(|operation| match operation {
SolidHistoryOperation::Brep(value) => world_point(value.base.transform, [0.0; 3]),
_ => None,
})
{
geometry.extend([
crate::entities::common::edit_prop(
t!("Position X").as_ref(),
PROP_POSITION_X,
position.x,
),
crate::entities::common::edit_prop(
t!("Position Y").as_ref(),
PROP_POSITION_Y,
position.y,
),
crate::entities::common::edit_prop(
t!("Position Z").as_ref(),
PROP_POSITION_Z,
position.z,
),
]);
}
vec![
PropSection {
title: t!("Geometry").into_owned(),
props: geometry,
},
PropSection {
title: t!("Solid History").into_owned(),
props: vec![
Property {
label: t!("History").into_owned(),
field: PROP_HISTORY,
value: PropValue::Choice {
selected: if record_history { "Record" } else { "None" }.to_string(),
options: vec!["None".to_string(), "Record".to_string()],
},
},
Property {
label: t!("Show History").into_owned(),
field: PROP_SHOW_HISTORY,
value: if show_history_editable {
PropValue::Choice {
selected: if show_history { "Yes" } else { "No" }.to_string(),
options: vec!["No".to_string(), "Yes".to_string()],
}
} else {
PropValue::ReadOnly(if show_history { "Yes" } else { "No" }.to_string())
},
},
],
},
]
}
fn cylinder_properties( fn cylinder_properties(
document: &acadrust::CadDocument, document: &acadrust::CadDocument,
handle: acadrust::Handle, handle: acadrust::Handle,
@@ -1390,10 +1491,10 @@ pub fn primitive_properties(
document: &acadrust::CadDocument, document: &acadrust::CadDocument,
handle: acadrust::Handle, handle: acadrust::Handle,
) -> Vec<PropSection> { ) -> Vec<PropSection> {
let Some(operation) = document.solid_history_operation(handle) else { let Some(operation) = primitive_property_operation(document, handle) else {
return Vec::new(); return brep_properties(document, handle);
}; };
match operation { match &operation {
SolidHistoryOperation::Box(value) => { SolidHistoryOperation::Box(value) => {
rectangular_properties(document, handle, value, "Box") rectangular_properties(document, handle, value, "Box")
} }
@@ -1411,7 +1512,8 @@ pub fn primitive_properties(
extrusion_properties(document, handle, value) extrusion_properties(document, handle, value)
} }
SolidHistoryOperation::Revolve(value) => revolve_properties(document, handle, value), SolidHistoryOperation::Revolve(value) => revolve_properties(document, handle, value),
_ => Vec::new(), SolidHistoryOperation::Brep(_) => brep_properties(document, handle),
_ => brep_properties(document, handle),
} }
} }
@@ -2137,11 +2239,50 @@ fn set_pyramid_sides(value: &mut SolidHistoryPyramid, sides: i32) -> bool {
true true
} }
fn apply_brep_position_property(
value: &mut SolidHistoryBrep,
field: &str,
text: &str,
) -> Option<bool> {
let axis = match field {
PROP_POSITION_X => 0,
PROP_POSITION_Y => 1,
PROP_POSITION_Z => 2,
_ => return None,
};
let Some(target) = crate::entities::common::parse_length(text) else {
return Some(false);
};
if !target.is_finite() {
return Some(false);
}
let Some(current) = matrix(value.base.transform) else {
return Some(false);
};
let position = current.transform_point3(glam::DVec3::ZERO);
let mut next = position;
next[axis] = target;
if next == position {
return Some(true);
}
let updated = glam::DMat4::from_translation(next - position) * current;
if !updated.is_finite() || updated.determinant().abs() <= 1e-12 {
return Some(false);
}
value.base.transform = updated.to_cols_array();
Some(true)
}
pub fn apply_primitive_property( pub fn apply_primitive_property(
operation: &mut SolidHistoryOperation, operation: &mut SolidHistoryOperation,
field: &str, field: &str,
value: &str, value: &str,
) -> bool { ) -> bool {
if let SolidHistoryOperation::Brep(brep_value) = operation {
if let Some(applied) = apply_brep_position_property(brep_value, field, value) {
return applied;
}
}
if let SolidHistoryOperation::Box(rectangular_value) if let SolidHistoryOperation::Box(rectangular_value)
| SolidHistoryOperation::Wedge(rectangular_value) = operation | SolidHistoryOperation::Wedge(rectangular_value) = operation
{ {
@@ -3037,6 +3178,149 @@ fn grip(
} }
} }
pub fn fillet_radius_grip(body: &Body, radius: f64) -> Option<GripDef> {
if !radius.is_finite() || radius <= 0.0 {
return None;
}
for (face_key, face) in body.faces.iter() {
let Some(Surface::Cylinder(cylinder)) = body.surfaces.get(face.surface) else {
continue;
};
if (cylinder.radius.abs() - radius).abs() > radius.max(1.0) * 1.0e-7 {
continue;
}
let axis = glam::DVec3::from_array(cylinder.base.normal()?).try_normalize()?;
let origin = glam::DVec3::from_array(cylinder.base.origin);
let points = body
.face_coedges(face_key)
.into_iter()
.filter_map(|coedge| body.coedges.get(coedge))
.filter_map(|coedge| body.edges.get(coedge.edge))
.flat_map(|edge| [edge.start, edge.end])
.filter_map(|vertex| body.vertices.get(vertex))
.map(|vertex| glam::DVec3::from_array(vertex.point))
.collect::<Vec<_>>();
let minimum = points
.iter()
.map(|point| (*point - origin).dot(axis))
.reduce(f64::min)?;
let maximum = points
.iter()
.map(|point| (*point - origin).dot(axis))
.reduce(f64::max)?;
let center = origin + axis * ((minimum + maximum) * 0.5);
let radial = points
.iter()
.map(|point| *point - center - axis * (*point - center).dot(axis))
.find_map(|radial| radial.try_normalize())?;
return Some(grip(
GRIP_FILLET_RADIUS,
center + radial * radius,
GripShape::Square,
Some(radial),
));
}
None
}
pub fn chamfer_distance_grips(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
value: &SolidHistoryChamfer,
) -> Vec<GripDef> {
let Some(operations) = document.solid_history_operations(handle) else {
return Vec::new();
};
let Some(chamfer_index) = operations
.iter()
.rposition(|operation| matches!(operation, SolidHistoryOperation::Chamfer(_)))
else {
return Vec::new();
};
let Ok(source) = cadkernel::acis::rebuild_history(&operations[..chamfer_index]) else {
return Vec::new();
};
let edges = source.edge_keys().collect::<Vec<_>>();
let faces = source.face_keys().collect::<Vec<_>>();
let Some(edge) = value
.edges
.first()
.and_then(|ordinal| usize::try_from(*ordinal).ok())
.and_then(|ordinal| edges.get(ordinal).copied())
.and_then(|edge| source.edges.get(edge))
else {
return Vec::new();
};
let Some(base_face) = usize::try_from(value.base_face)
.ok()
.and_then(|ordinal| faces.get(ordinal).copied())
else {
return Vec::new();
};
let adjacent = edge
.coedges
.iter()
.filter_map(|coedge| source.coedges.get(*coedge))
.filter_map(|coedge| source.loops.get(coedge.owner))
.map(|edge_loop| edge_loop.owner)
.collect::<Vec<_>>();
let Some(other_face) = adjacent.iter().copied().find(|face| *face != base_face) else {
return Vec::new();
};
let outward = |face_key| {
let face = source.faces.get(face_key)?;
let Surface::Plane(plane) = source.surfaces.get(face.surface)? else {
return None;
};
let normal = glam::DVec3::from_array(plane.normal()?).try_normalize()?;
Some(if face.forward { normal } else { -normal })
};
let Some(base_normal) = outward(base_face) else {
return Vec::new();
};
let Some(other_normal) = outward(other_face) else {
return Vec::new();
};
let dot = base_normal.dot(other_normal);
let Some(base_inward) = (-(other_normal - base_normal * dot)).try_normalize() else {
return Vec::new();
};
let Some(other_inward) = (-(base_normal - other_normal * dot)).try_normalize() else {
return Vec::new();
};
let Some(start) = source.vertices.get(edge.start) else {
return Vec::new();
};
let Some(end) = source.vertices.get(edge.end) else {
return Vec::new();
};
let midpoint =
(glam::DVec3::from_array(start.point) + glam::DVec3::from_array(end.point)) * 0.5;
[
(
GRIP_CHAMFER_DISTANCE1,
value.base_distance,
base_inward,
),
(
GRIP_CHAMFER_DISTANCE2,
value.other_distance,
other_inward,
),
]
.into_iter()
.filter(|(_, distance, _)| distance.is_finite() && *distance > 0.0)
.map(|(id, distance, axis)| {
grip(
id,
midpoint + axis * distance,
GripShape::Square,
Some(axis),
)
})
.collect()
}
pub fn primitive_grips( pub fn primitive_grips(
document: &acadrust::CadDocument, document: &acadrust::CadDocument,
handle: acadrust::Handle, handle: acadrust::Handle,
@@ -3044,6 +3328,21 @@ pub fn primitive_grips(
let Some(operation) = document.solid_history_operation(handle) else { let Some(operation) = document.solid_history_operation(handle) else {
return Vec::new(); return Vec::new();
}; };
if let SolidHistoryOperation::Fillet(value) = operation {
let Some(radius) = value.radii.first().copied() else {
return Vec::new();
};
let Some(EntityType::Solid3D(solid)) = document.get_entity(handle) else {
return Vec::new();
};
return crate::scene::convert::solid3d_tess::kernel_body(solid)
.and_then(|body| fillet_radius_grip(&body, radius))
.into_iter()
.collect();
}
if let SolidHistoryOperation::Chamfer(value) = operation {
return chamfer_distance_grips(document, handle, value);
}
let mut grips = Vec::new(); let mut grips = Vec::new();
let mut add = |id, transform, point, shape, axis: Option<[f64; 3]>| { let mut add = |id, transform, point, shape, axis: Option<[f64; 3]>| {
if let Some(world) = world_point(transform, point) { if let Some(world) = world_point(transform, point) {
@@ -3664,3 +3963,32 @@ pub fn brep_op(body: &Body) -> SolidHistoryOperation {
..SolidHistoryBrep::default() ..SolidHistoryBrep::default()
}) })
} }
pub fn fillet_op(edges: Vec<i32>, radius: f64) -> SolidHistoryOperation {
SolidHistoryOperation::Fillet(SolidHistoryFillet {
base: base(glam::DMat4::IDENTITY.to_cols_array()),
operation_major: 1,
method: 0,
edges,
radii: vec![radius],
..SolidHistoryFillet::default()
})
}
pub fn chamfer_op(
edges: Vec<i32>,
base_face: i32,
base_distance: f64,
other_distance: f64,
) -> SolidHistoryOperation {
SolidHistoryOperation::Chamfer(SolidHistoryChamfer {
base: base(glam::DMat4::IDENTITY.to_cols_array()),
operation_major: 1,
method: 0,
base_distance,
other_distance,
edges,
base_face,
..SolidHistoryChamfer::default()
})
}
+11 -2
View File
@@ -365,13 +365,18 @@ pub enum Bool {
/// missing, and passing that on unchanged is the point: a half-done boolean /// missing, and passing that on unchanged is the point: a half-done boolean
/// looks finished. /// looks finished.
pub fn boolean(op: Bool, a: &Body, b: &Body) -> Option<Body> { pub fn boolean(op: Bool, a: &Body, b: &Body) -> Option<Body> {
boolean_result(op, a, b).ok()
}
/// Combine two solids while retaining the kernel's exact refusal reason.
pub fn boolean_result(op: Bool, a: &Body, b: &Body) -> Result<Body, brep::Snag> {
let how = match op { let how = match op {
Bool::Union => brep::Operation::Union, Bool::Union => brep::Operation::Union,
Bool::Subtract => brep::Operation::Difference, Bool::Subtract => brep::Operation::Difference,
Bool::Intersect => brep::Operation::Intersection, Bool::Intersect => brep::Operation::Intersection,
}; };
let tolerance = brep::operation_tolerance(&[a, b]); let tolerance = brep::operation_tolerance(&[a, b]);
brep::combine(a.clone(), b.clone(), how, tolerance).ok() brep::combine(a.clone(), b.clone(), how, tolerance)
} }
// ── Tessellation ──────────────────────────────────────────────────────────── // ── Tessellation ────────────────────────────────────────────────────────────
@@ -470,7 +475,11 @@ pub fn display_from_solid(
) )
}) })
.collect(); .collect();
Some((mesh_from_tessellation(tessellation, color)?, wires, center)) let mut mesh = mesh_from_tessellation(tessellation, color)?;
if let Some(properties) = cadkernel::brep::analytic_mass_properties(body) {
mesh.apply_mass_properties(properties);
}
Some((mesh, wires, center))
} }
/// The middle of a body, for a caller needing a point to turn or scale about. /// The middle of a body, for a caller needing a point to turn or scale about.
+202 -11
View File
@@ -807,11 +807,39 @@ impl Scene {
true true
} }
pub fn append_solid_history(
&mut self,
handle: Handle,
operation: acadrust::objects::SolidHistoryOperation,
) -> bool {
let previous = self
.document
.solid_history_graph(handle)
.map(|graph| graph.nodes)
.unwrap_or_default();
self.record_solid_history_before(handle);
let Some(graph) = self.document.append_solid_history(handle, operation) else {
return false;
};
for node in graph.nodes {
if !previous.contains(&node) {
self.record_undo_object_before(node, None);
}
}
self.sync_solid_reference_point(handle);
true
}
pub(crate) fn sync_solid_reference_point(&mut self, handle: Handle) { pub(crate) fn sync_solid_reference_point(&mut self, handle: Handle) {
let reference = self let reference = self
.document .document
.solid_history_operation(handle) .solid_history_operations(handle)
.and_then(crate::scene::model::solid_history::reference_point); .and_then(|operations| {
operations
.iter()
.rev()
.find_map(crate::scene::model::solid_history::reference_point)
});
let Some(reference) = reference else { let Some(reference) = reference else {
return; return;
}; };
@@ -860,8 +888,31 @@ impl Scene {
(EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => { (EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => {
cadkernel::acis::rebuild_extrusion_with_mode(value, true).ok() cadkernel::acis::rebuild_extrusion_with_mode(value, true).ok()
} }
(EntityType::Surface(_), SolidHistoryOperation::Loft(_)) (EntityType::Surface(_), SolidHistoryOperation::Loft(_)) => {
| (EntityType::Solid3D(_), _) => cadkernel::acis::rebuild_body(operation).ok(), cadkernel::acis::rebuild_body(operation).ok()
}
(EntityType::Solid3D(_), _) => {
let mut operations = self.document.solid_history_operations(handle)?;
let replacement_id = operation.base().map(|base| {
if base.eval.node_id > 0 {
base.eval.node_id
} else {
base.step_id
}
})?;
let target = operations.iter_mut().find(|candidate| {
candidate.base().is_some_and(|base| {
let node_id = if base.eval.node_id > 0 {
base.eval.node_id
} else {
base.step_id
};
node_id == replacement_id
})
})?;
*target = operation.clone();
cadkernel::acis::rebuild_history(&operations).ok()
}
_ => None, _ => None,
} }
} }
@@ -914,7 +965,7 @@ impl Scene {
self.record_solid_history_before(handle); self.record_solid_history_before(handle);
if self if self
.document .document
.update_solid_history(handle, operation) .update_solid_history_step(handle, operation)
.is_none() .is_none()
{ {
return false; return false;
@@ -1051,16 +1102,71 @@ impl Scene {
value: &str, value: &str,
) -> bool { ) -> bool {
self.record_solid_history_before(handle); self.record_solid_history_before(handle);
let mut created = false;
if self.document.solid_history_graph(handle).is_none() {
let create = match field {
crate::scene::model::solid_history::PROP_HISTORY => {
if value.eq_ignore_ascii_case("Record") {
true
} else if value.eq_ignore_ascii_case("None") {
return false;
} else {
return false;
}
}
crate::scene::model::solid_history::PROP_SHOW_HISTORY
if self.document.header.show_solid_history.clamp(0, 2) == 1 =>
{
if value.eq_ignore_ascii_case("Yes") {
true
} else if value.eq_ignore_ascii_case("No") {
return false;
} else {
return false;
}
}
_ => return false,
};
if create {
if !matches!(self.document.get_entity(handle), Some(EntityType::Solid3D(_))) {
return false;
}
self.restore_solid_models(&[handle]);
let Some(body) = self.solid_models.get(&handle).cloned() else {
return false;
};
if self.is_recording_undo() {
let before = self.document.get_entity_arc(handle);
self.record_undo_before(handle, before);
}
if !self.create_solid_history(
handle,
crate::scene::model::solid_history::brep_op(&body),
) {
return false;
}
// A solid without a graph was displayed as History=None.
// Preserve that object state before applying the requested
// choice, independent of the drawing-wide SOLIDHIST setting.
crate::scene::model::solid_history::apply_history_choice(
&mut self.document,
handle,
crate::scene::model::solid_history::PROP_HISTORY,
"None",
);
created = true;
}
}
let applied = crate::scene::model::solid_history::apply_history_choice( let applied = crate::scene::model::solid_history::apply_history_choice(
&mut self.document, &mut self.document,
handle, handle,
field, field,
value, value,
); );
if applied { if created || applied {
self.bump_entities(&[(handle, ChangeKind::Modified)]); self.bump_entities(&[(handle, ChangeKind::Modified)]);
} }
applied created || applied
} }
fn apply_solid_history_grip( fn apply_solid_history_grip(
@@ -1072,6 +1178,79 @@ impl Scene {
let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else { let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else {
return false; return false;
}; };
if grip_id == crate::scene::model::solid_history::GRIP_FILLET_RADIUS {
let acadrust::objects::SolidHistoryOperation::Fillet(value) = &mut operation else {
return false;
};
let Some(radius) = value.radii.first().copied() else {
return false;
};
self.restore_solid_models(&[handle]);
let Some(definition) = self
.solid_models
.get(&handle)
.and_then(|body| {
crate::scene::model::solid_history::fillet_radius_grip(body, radius)
})
else {
return false;
};
let Some(axis) = definition.axis else {
return false;
};
let change = match apply {
GripApply::Absolute(world) => (world - definition.world).dot(axis),
GripApply::Translate(delta) => delta.dot(axis),
};
let radius = radius + change;
if !radius.is_finite() || radius <= 1.0e-6 {
return false;
}
value.radii.clear();
value.radii.push(radius);
return self.preview_solid_history(handle, operation);
}
if matches!(
grip_id,
crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1
| crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE2
) {
let definitions = {
let acadrust::objects::SolidHistoryOperation::Chamfer(value) = &operation else {
return false;
};
crate::scene::model::solid_history::chamfer_distance_grips(
&self.document,
handle,
value,
)
};
let Some(definition) = definitions.into_iter().find(|grip| grip.id == grip_id) else {
return false;
};
let Some(axis) = definition.axis else {
return false;
};
let change = match apply {
GripApply::Absolute(world) => (world - definition.world).dot(axis),
GripApply::Translate(delta) => delta.dot(axis),
};
let acadrust::objects::SolidHistoryOperation::Chamfer(value) = &mut operation else {
return false;
};
let distance = if grip_id
== crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1
{
&mut value.base_distance
} else {
&mut value.other_distance
};
*distance += change;
if !distance.is_finite() || *distance <= 1.0e-6 {
return false;
}
return self.preview_solid_history(handle, operation);
}
if !crate::scene::model::solid_history::apply_primitive_grip( if !crate::scene::model::solid_history::apply_primitive_grip(
&mut operation, &mut operation,
grip_id, grip_id,
@@ -1088,7 +1267,12 @@ impl Scene {
field: &str, field: &str,
value: &str, value: &str,
) -> bool { ) -> bool {
let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else { let Some(mut operation) =
crate::scene::model::solid_history::primitive_property_operation(
&self.document,
handle,
)
else {
return false; return false;
}; };
if !crate::scene::model::solid_history::apply_primitive_property( if !crate::scene::model::solid_history::apply_primitive_property(
@@ -1185,9 +1369,7 @@ impl Scene {
generator.source = source; generator.source = source;
} }
} }
set.metrics.centroid[0] += delta[0]; set.metrics.translate(delta);
set.metrics.centroid[1] += delta[1];
set.metrics.centroid[2] += delta[2];
set.recompute_aabb(); set.recompute_aabb();
} }
@@ -1337,5 +1519,14 @@ impl Scene {
self.images.insert(handle, model); self.images.insert(handle, model);
} }
} }
// A grip edit can move geometry used as the boundary of an associative hatch.
// The source entity has already been mutated above, so rebuild any dependent
// hatch from the live document geometry immediately.
let source_change = [(handle, ChangeKind::Modified)];
let hatch_changes = self.refresh_associative_hatches(&source_change);
if !hatch_changes.is_empty() {
self.bump_entities(&hatch_changes);
}
} }
} }
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff. Show more