forked from 1b-agent/opencadstudio-upstream
Merge pull request #986 from ramox81/codex/revolve-command-properties
Complete REVOLVE workflow and properties
This commit is contained in:
17 files changed
+1309
-125
No files matched your search
Generated
+1
-1
@@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
[[package]]
|
||||
name = "cadkernel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=93f7aa4efa61eeaf3aeefbdbaff6078283820df6#93f7aa4efa61eeaf3aeefbdbaff6078283820df6"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=5fc881c0f6fbc7fb96d483cd3a4efe145ba6133f#5fc881c0f6fbc7fb96d483cd3a4efe145ba6133f"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"cavalier_contours",
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ rfd = "0.17"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
env_logger = "0.11"
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "393d6c0857447f9ca678bed97a612707f0a842c6", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "93f7aa4efa61eeaf3aeefbdbaff6078283820df6", features = ["acis", "offset"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "5fc881c0f6fbc7fb96d483cd3a4efe145ba6133f", features = ["acis", "offset"] }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||
|
||||
@@ -715,7 +715,9 @@ common =
|
||||
.quadrant = Quadrant
|
||||
.quality = Quality
|
||||
.revolve-could-not-revolve-profile = REVOLVE: could not revolve profile.
|
||||
.revolve-created-objects-failed-sources = REVOLVE: __ocs_arg_created__ object(s) created; __ocs_arg_failed__ source(s) could not be revolved.
|
||||
.revolve-entity-not-found = REVOLVE: entity not found.
|
||||
.revolve-no-selected-object-could-be-revolved = REVOLVE: no selected object could be revolved with the requested options.
|
||||
.revolve-solid-created-0 = REVOLVE: solid created (__ocs_fmt_0__°).
|
||||
.radians = Radians
|
||||
.radius = Radius
|
||||
@@ -2064,7 +2066,14 @@ insert =
|
||||
.revolve-angle-of-revolution-360 = REVOLVE Angle of revolution <360>:
|
||||
.revolve-axis-end-point = REVOLVE Axis end point:
|
||||
.revolve-axis-start-point = REVOLVE Axis start point:
|
||||
.revolve-creation-mode-solid-surface = REVOLVE Creation mode [Solid/Surface]
|
||||
.revolve-enter-angle-expression = REVOLVE Enter angle expression:
|
||||
.revolve-select-line-ray-or-construction-line-for-axis = REVOLVE Select line, ray, or construction line for axis:
|
||||
.revolve-select-objects-or-mode = REVOLVE Select objects to revolve or [Mode] (Enter to finish):
|
||||
.revolve-select-profile = REVOLVE Select profile:
|
||||
.revolve-specify-angle-or-options = REVOLVE Specify angle of revolution or [Start angle/Reverse/Expression]
|
||||
.revolve-specify-axis-start-or-options = REVOLVE Specify axis start point or [Object/X/Y/Z/Mode]:
|
||||
.revolve-specify-start-angle = REVOLVE Specify start angle
|
||||
.sweep-select-path-line-arc-lwpolyline = SWEEP Select path (Line, Arc, LwPolyline):
|
||||
.sweep-select-profile-to-sweep = SWEEP Select profile to sweep:
|
||||
.set-base-point =
|
||||
@@ -2434,6 +2443,7 @@ properties =
|
||||
.alt-suppress-trailing-zeros = Alt suppress trailing zeros
|
||||
.ambient = Ambient
|
||||
.angle-format = Angle format
|
||||
.angle-of-revolution = Angle of revolution
|
||||
.angle-precision = Angle precision
|
||||
.annotation-scale = Annotation scale
|
||||
.annotative-scale = Annotative scale
|
||||
@@ -2449,6 +2459,12 @@ properties =
|
||||
.at-least = At least
|
||||
.attachment-type = Attachment type
|
||||
.attachment = Attachment
|
||||
.axis-direction-x = Axis direction X
|
||||
.axis-direction-y = Axis direction Y
|
||||
.axis-direction-z = Axis direction Z
|
||||
.axis-position-x = Axis position X
|
||||
.axis-position-y = Axis position Y
|
||||
.axis-position-z = Axis position Z
|
||||
.arrowhead-size = Arrowhead Size
|
||||
.attenuation = Attenuation
|
||||
.attribute-definition = Attribute Definition
|
||||
@@ -2879,6 +2895,7 @@ properties =
|
||||
.show = Show
|
||||
.show-clipping = Show Clipping
|
||||
.show-cropping = Show Cropping
|
||||
.show-history = Show History
|
||||
.show-intensity = Show Intensity
|
||||
.show-clipped = Show clipped
|
||||
.show-image = Show image
|
||||
@@ -2886,6 +2903,8 @@ properties =
|
||||
.singleline = SingleLine
|
||||
.softness = Softness
|
||||
.solid = Solid
|
||||
.solid-history = Solid History
|
||||
.solid-type = Solid type
|
||||
.solid3d = Solid3D
|
||||
.source-files = Source Files
|
||||
.source-block = Source block
|
||||
|
||||
@@ -702,7 +702,9 @@ common =
|
||||
.quadrant = Çeyrek Nokta
|
||||
.quality = Kalite
|
||||
.revolve-could-not-revolve-profile = REVOLVE: Profil döndürülemedi.
|
||||
.revolve-created-objects-failed-sources = REVOLVE: __ocs_arg_created__ nesne oluşturuldu; __ocs_arg_failed__ kaynak döndürülemedi.
|
||||
.revolve-entity-not-found = REVOLVE: Nesne bulunamadı.
|
||||
.revolve-no-selected-object-could-be-revolved = REVOLVE: Seçilen nesnelerin hiçbiri istenen seçeneklerle döndürülemedi.
|
||||
.revolve-solid-created-0 = REVOLVE: Katı oluşturuldu (__ocs_fmt_0__°).
|
||||
.radians = Radyan
|
||||
.radius = Yarıçap
|
||||
@@ -2030,7 +2032,14 @@ insert =
|
||||
.revolve-angle-of-revolution-360 = REVOLVE Döndürme açısı <360>:
|
||||
.revolve-axis-end-point = REVOLVE Eksen bitiş noktası:
|
||||
.revolve-axis-start-point = REVOLVE Eksen başlangıç noktası:
|
||||
.revolve-creation-mode-solid-surface = REVOLVE Oluşturma modu [Solid/Surface]
|
||||
.revolve-enter-angle-expression = REVOLVE Açı ifadesini girin:
|
||||
.revolve-select-line-ray-or-construction-line-for-axis = REVOLVE Eksen için çizgi, ışın veya sonsuz çizgi seçin:
|
||||
.revolve-select-objects-or-mode = REVOLVE Döndürülecek nesneleri seçin veya [Mode] (bitirmek için Enter):
|
||||
.revolve-select-profile = REVOLVE Profili seçin:
|
||||
.revolve-specify-angle-or-options = REVOLVE Döndürme açısını belirtin veya [Start angle/Reverse/Expression]
|
||||
.revolve-specify-axis-start-or-options = REVOLVE Eksen başlangıç noktasını belirtin veya [Object/X/Y/Z/Mode]:
|
||||
.revolve-specify-start-angle = REVOLVE Başlangıç açısını belirtin
|
||||
.sweep-select-path-line-arc-lwpolyline = SWEEP Yolu seçin (Çizgi, Yay, Hafif Çoklu Çizgi):
|
||||
.sweep-select-profile-to-sweep = SWEEP Süpürülecek profili seçin:
|
||||
.set-base-point = Taban Noktasını Ayarla
|
||||
@@ -2375,6 +2384,7 @@ properties =
|
||||
.alt-suppress-trailing-zeros = Alternatif birimlerde sondaki sıfırları gizle
|
||||
.ambient = Ortam
|
||||
.angle-format = Açı biçimi
|
||||
.angle-of-revolution = Döndürme açısı
|
||||
.angle-precision = Açı hassasiyeti
|
||||
.annotation-scale = Açıklama ölçeği
|
||||
.annotative-scale = Açıklamalı ölçek
|
||||
@@ -2390,6 +2400,12 @@ properties =
|
||||
.at-least = En az
|
||||
.attachment-type = Bağlantı türü
|
||||
.attachment = Bağlantı
|
||||
.axis-direction-x = Eksen yönü X
|
||||
.axis-direction-y = Eksen yönü Y
|
||||
.axis-direction-z = Eksen yönü Z
|
||||
.axis-position-x = Eksen konumu X
|
||||
.axis-position-y = Eksen konumu Y
|
||||
.axis-position-z = Eksen konumu Z
|
||||
.arrowhead-size = Ok Ucu Boyutu
|
||||
.attenuation = Zayıflama
|
||||
.attribute-definition = Öznitelik Tanımı
|
||||
@@ -2819,6 +2835,7 @@ properties =
|
||||
.show = Göster
|
||||
.show-clipping = Kırpmayı Göster
|
||||
.show-cropping = Kırpmayı Göster
|
||||
.show-history = Geçmişi Göster
|
||||
.show-intensity = Yoğunluğu Göster
|
||||
.show-clipped = Kırpılanı göster
|
||||
.show-image = Görüntüyü göster
|
||||
@@ -2826,6 +2843,8 @@ properties =
|
||||
.singleline = Tek Satır
|
||||
.softness = Yumuşaklık
|
||||
.solid = Katı
|
||||
.solid-history = Katı Geçmişi
|
||||
.solid-type = Katı türü
|
||||
.solid3d = 3B Katı
|
||||
.source-files = Kaynak Dosyalar
|
||||
.source-block = Kaynak blok
|
||||
|
||||
+115
-37
@@ -3827,66 +3827,144 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
// ── REVOLVE ────────────────────────────────────────────────────
|
||||
CmdResult::RevolveEntity {
|
||||
handle,
|
||||
CmdResult::RevolveEntities {
|
||||
handles,
|
||||
axis_start,
|
||||
axis_end,
|
||||
angle_deg,
|
||||
angle,
|
||||
start_angle,
|
||||
mode,
|
||||
color: _,
|
||||
} => {
|
||||
if self.reject_locked_edit(i, handle) {
|
||||
if handles.is_empty() {
|
||||
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::none();
|
||||
}
|
||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||
let delete_sources = self.tabs[i].scene.document.header.delete_objects;
|
||||
use crate::command::ExtrudeMode;
|
||||
use crate::modules::insert::solid3d_cmds::{
|
||||
empty_revolved_surface, empty_solid3d,
|
||||
};
|
||||
use crate::scene::model::sweep_model;
|
||||
|
||||
let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
|
||||
if let Some(entity) = entity_opt {
|
||||
let from = [
|
||||
axis_start.x as f64,
|
||||
axis_start.y as f64,
|
||||
axis_start.z as f64,
|
||||
];
|
||||
let to = [axis_end.x as f64, axis_end.y as f64, axis_end.z as f64];
|
||||
let angle = (angle_deg as f64).to_radians();
|
||||
let result = sweep_model::revolve_history(&entity, from, to, angle)
|
||||
let from = axis_start.to_array();
|
||||
let to = axis_end.to_array();
|
||||
let mut editable_handles = Vec::with_capacity(handles.len());
|
||||
let mut failed = 0usize;
|
||||
for handle in handles {
|
||||
if self.reject_locked_edit(i, handle) {
|
||||
failed += 1;
|
||||
} else {
|
||||
editable_handles.push(handle);
|
||||
}
|
||||
}
|
||||
let pending = if editable_handles.is_empty() {
|
||||
None
|
||||
} else {
|
||||
self.begin_undo(i, "REVOLVE", editable_handles.len(), true)
|
||||
};
|
||||
let mut created_handles = Vec::new();
|
||||
let mut consumed = Vec::new();
|
||||
for handle in &editable_handles {
|
||||
let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned()
|
||||
else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let Some((_, closed)) = sweep_model::extrusion_profile_of(&entity) else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let creates_surface = mode == ExtrudeMode::Surface || !closed;
|
||||
let created = if creates_surface {
|
||||
let Some(body) =
|
||||
sweep_model::revolved_surface(&entity, from, to, angle, start_angle)
|
||||
else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
self.add_surface_model(
|
||||
empty_revolved_surface(
|
||||
&entity,
|
||||
axis_start,
|
||||
axis_end,
|
||||
angle,
|
||||
start_angle,
|
||||
),
|
||||
body,
|
||||
)
|
||||
} else {
|
||||
let result = sweep_model::revolve_history(
|
||||
&entity,
|
||||
from,
|
||||
to,
|
||||
angle,
|
||||
start_angle,
|
||||
)
|
||||
.and_then(|history| {
|
||||
cadkernel::acis::rebuild_body(&history)
|
||||
.ok()
|
||||
.map(|solid| (solid, history))
|
||||
})
|
||||
.or_else(|| {
|
||||
sweep_model::revolved(&entity, from, to, angle).map(|solid| {
|
||||
let history =
|
||||
crate::scene::model::solid_history::brep_op(&solid);
|
||||
(solid, history)
|
||||
})
|
||||
sweep_model::revolved(&entity, from, to, angle, start_angle).map(
|
||||
|solid| {
|
||||
let history =
|
||||
crate::scene::model::solid_history::brep_op(&solid);
|
||||
(solid, history)
|
||||
},
|
||||
)
|
||||
});
|
||||
if let Some((solid, history)) = result {
|
||||
let pending = self.begin_undo(i, "REVOLVE", 1, true);
|
||||
let created = self.add_solid_model(empty_solid3d(), solid, history);
|
||||
if !created.is_null() {
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(
|
||||
crate::tf!("REVOLVE: solid created ({:.0}°).", angle_deg).as_ref(),
|
||||
);
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
let Some((solid, history)) = result else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
self.add_solid_model(empty_solid3d(), solid, history)
|
||||
};
|
||||
if created.is_null() {
|
||||
failed += 1;
|
||||
} else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("REVOLVE: could not revolve profile.").as_ref());
|
||||
created_handles.push(created);
|
||||
if delete_sources {
|
||||
consumed.push(*handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
if delete_sources && !created_handles.is_empty() {
|
||||
consumed.sort_unstable_by_key(|handle| handle.value());
|
||||
consumed.dedup();
|
||||
self.tabs[i].scene.erase_entities(&consumed);
|
||||
}
|
||||
if !created_handles.is_empty() {
|
||||
self.tabs[i].scene.deselect_all();
|
||||
for handle in &created_handles {
|
||||
self.tabs[i].scene.select_entity(*handle, true);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"REVOLVE: created %{created} object(s); %{failed} source(s) could not be revolved.",
|
||||
created = created_handles.len(),
|
||||
failed = failed
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
} else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("REVOLVE: entity not found.").as_ref());
|
||||
self.command_line.push_error(
|
||||
crate::t!("REVOLVE: no selected object could be revolved with the requested options.")
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.refresh_properties();
|
||||
}
|
||||
// ── SWEEP ─────────────────────────────────────────────────────
|
||||
CmdResult::SweepEntity {
|
||||
|
||||
@@ -794,8 +794,18 @@ impl OpenCADStudio {
|
||||
// ── REVOLVE ────────────────────────────────────────────────────
|
||||
"REVOLVE" => {
|
||||
use crate::modules::insert::solid3d_cmds::RevolveCommand;
|
||||
let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
|
||||
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
||||
let cmd = RevolveCommand::new(color);
|
||||
let isolines = self.tabs[i].scene.document.header.isolines.max(0) as usize;
|
||||
let mut cmd = RevolveCommand::new(color, isolines);
|
||||
if !selected.is_empty() {
|
||||
cmd.set_preselection(
|
||||
selected
|
||||
.iter()
|
||||
.map(|(handle, entity)| (*handle, (*entity).clone()))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
+11
-2
@@ -36,7 +36,13 @@ impl super::OpenCADStudio {
|
||||
return Handle::NULL;
|
||||
};
|
||||
self.tabs[i].scene.create_solid_history(handle, history);
|
||||
self.tabs[i].scene.register_solid_model(handle, solid);
|
||||
if !self.tabs[i].scene.register_solid_model(handle, solid) {
|
||||
// A command result is not successful until it has renderable
|
||||
// geometry. Roll the new entity back so DELOBJ cannot consume a
|
||||
// visible source in exchange for an invisible result.
|
||||
self.tabs[i].scene.rollback_new_entities(&[handle]);
|
||||
return Handle::NULL;
|
||||
}
|
||||
handle
|
||||
}
|
||||
|
||||
@@ -63,7 +69,10 @@ impl super::OpenCADStudio {
|
||||
let Some(handle) = self.commit_entity_handle(entity) else {
|
||||
return Handle::NULL;
|
||||
};
|
||||
self.tabs[i].scene.register_solid_model(handle, surface);
|
||||
if !self.tabs[i].scene.register_solid_model(handle, surface) {
|
||||
self.tabs[i].scene.rollback_new_entities(&[handle]);
|
||||
return Handle::NULL;
|
||||
}
|
||||
handle
|
||||
}
|
||||
|
||||
|
||||
@@ -660,7 +660,16 @@ impl OpenCADStudio {
|
||||
// the sign of the cursor's side relative to the reference, so the
|
||||
// committed direction matches the drag (the box shows magnitude
|
||||
// only). Commands receive an already-signed string.
|
||||
let text = self.dyn_sign_angle_text(i, text);
|
||||
let auto_sign = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|command| command.dyn_auto_sign_angle())
|
||||
.unwrap_or(true);
|
||||
let text = if auto_sign {
|
||||
self.dyn_sign_angle_text(i, text)
|
||||
} else {
|
||||
text
|
||||
};
|
||||
let result = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_mut()
|
||||
|
||||
+39
-14
@@ -462,13 +462,19 @@ impl OpenCADStudio {
|
||||
// clearing Z afterwards shifts the picked point on screen in an
|
||||
// oblique view. Plain paper space still uses the camera target plane.
|
||||
let plane = if self.tabs[i].editing_model_space() {
|
||||
Some(match self.tabs[i].active_ucs.as_ref() {
|
||||
Some(ucs) => (
|
||||
ucs_z_axis(ucs),
|
||||
glam::DVec3::new(ucs.origin.x, ucs.origin.y, ucs.origin.z),
|
||||
),
|
||||
None => (glam::DVec3::Z, glam::DVec3::ZERO),
|
||||
})
|
||||
self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.and_then(|command| command.cursor_plane())
|
||||
.or_else(|| {
|
||||
Some(match self.tabs[i].active_ucs.as_ref() {
|
||||
Some(ucs) => (
|
||||
ucs_z_axis(ucs),
|
||||
glam::DVec3::new(ucs.origin.x, ucs.origin.y, ucs.origin.z),
|
||||
),
|
||||
None => (glam::DVec3::Z, glam::DVec3::ZERO),
|
||||
})
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1908,12 +1914,17 @@ impl OpenCADStudio {
|
||||
&& !command.needs_tangent_pick()
|
||||
&& !command.is_selection_gathering()
|
||||
});
|
||||
let uses_command_cursor_plane = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.and_then(|command| command.cursor_plane())
|
||||
.is_some();
|
||||
let axis_lock = if let Some(base) = self.last_point {
|
||||
self.active_axis_lock(
|
||||
i,
|
||||
cursor_world,
|
||||
base,
|
||||
wants_point && !is_window_corner,
|
||||
wants_point && !is_window_corner && !uses_command_cursor_plane,
|
||||
)
|
||||
} else {
|
||||
self.axis_lock_dir = None;
|
||||
@@ -2040,7 +2051,7 @@ impl OpenCADStudio {
|
||||
let osnap_locked = self.tabs[i]
|
||||
.snap_result
|
||||
.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid);
|
||||
if !osnap_locked && !is_window_corner {
|
||||
if !osnap_locked && !is_window_corner && !uses_command_cursor_plane {
|
||||
if let Some(base) = self.last_point {
|
||||
let ucs_xf = self.tabs[i].ucs_xform();
|
||||
if self.ortho_mode {
|
||||
@@ -2070,7 +2081,10 @@ impl OpenCADStudio {
|
||||
};
|
||||
// Clamp to world XY only when no UCS is active; with a
|
||||
// UCS the point already lies on the UCS XY plane.
|
||||
if self.tabs[i].active_cmd.is_some() && self.tabs[i].active_ucs.is_none() {
|
||||
if self.tabs[i].active_cmd.is_some()
|
||||
&& self.tabs[i].active_ucs.is_none()
|
||||
&& !uses_command_cursor_plane
|
||||
{
|
||||
pt.z = 0.0;
|
||||
}
|
||||
pt
|
||||
@@ -3367,7 +3381,14 @@ impl OpenCADStudio {
|
||||
let mut pt = snap_hit.map(|s| s.world).unwrap_or(raw);
|
||||
// When no UCS is active clamp to world XY; with a UCS the point is
|
||||
// already constrained to that plane by the ray–plane intersection.
|
||||
if self.tabs[i].active_ucs.is_none() {
|
||||
let uses_command_cursor_plane = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.and_then(|command| command.cursor_plane())
|
||||
.is_some();
|
||||
let clamp_world_xy =
|
||||
self.tabs[i].active_ucs.is_none() && !uses_command_cursor_plane;
|
||||
if clamp_world_xy {
|
||||
pt.z = 0.0;
|
||||
}
|
||||
let axis_lock = if let Some(base) = self.last_point {
|
||||
@@ -3375,7 +3396,10 @@ impl OpenCADStudio {
|
||||
i,
|
||||
raw,
|
||||
base,
|
||||
!needs_entity_click && !needs_tan && !is_window_corner,
|
||||
!needs_entity_click
|
||||
&& !needs_tan
|
||||
&& !is_window_corner
|
||||
&& !uses_command_cursor_plane,
|
||||
)
|
||||
} else {
|
||||
self.axis_lock_dir = None;
|
||||
@@ -3383,7 +3407,7 @@ impl OpenCADStudio {
|
||||
};
|
||||
if let (Some(dir), Some(base)) = (axis_lock, self.last_point) {
|
||||
pt = axis_lock_apply(pt, base, dir);
|
||||
if self.tabs[i].active_ucs.is_none() {
|
||||
if clamp_world_xy {
|
||||
pt.z = 0.0;
|
||||
}
|
||||
}
|
||||
@@ -3403,11 +3427,12 @@ impl OpenCADStudio {
|
||||
};
|
||||
if let Some(h) = otrack {
|
||||
pt = h.aligned;
|
||||
if self.tabs[i].active_ucs.is_none() {
|
||||
if clamp_world_xy {
|
||||
pt.z = 0.0;
|
||||
}
|
||||
} else if axis_lock.is_none()
|
||||
&& !is_window_corner
|
||||
&& !uses_command_cursor_plane
|
||||
&& !snap_hit.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid)
|
||||
{
|
||||
// Object snap wins over ortho/polar — a snapped point
|
||||
|
||||
+18
-4
@@ -1443,12 +1443,14 @@ pub enum CmdResult {
|
||||
drag: Option<DVec3>,
|
||||
color: [f32; 4],
|
||||
},
|
||||
/// Revolve the profile entity `handle` around the given axis by `angle_deg`.
|
||||
RevolveEntity {
|
||||
handle: Handle,
|
||||
/// Revolve the profile entities around the given axis.
|
||||
RevolveEntities {
|
||||
handles: Vec<Handle>,
|
||||
axis_start: glam::DVec3,
|
||||
axis_end: glam::DVec3,
|
||||
angle_deg: f32,
|
||||
angle: f64,
|
||||
start_angle: f64,
|
||||
mode: ExtrudeMode,
|
||||
color: [f32; 4],
|
||||
},
|
||||
/// Sweep the profile entity `profile_handle` along `path_handle`.
|
||||
@@ -1776,6 +1778,12 @@ pub trait CadCommand: Send {
|
||||
None
|
||||
}
|
||||
|
||||
/// Override the model-space plane onto which viewport cursor rays are
|
||||
/// projected for this command step.
|
||||
fn cursor_plane(&self) -> Option<(DVec3, DVec3)> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Mid-command Ctrl+Z: a multi-point drawing command can take the undo
|
||||
/// itself (PLINE pops its last vertex) instead of the document undo
|
||||
/// swallowing the whole in-progress object. `None` (default) lets the
|
||||
@@ -2161,6 +2169,12 @@ pub trait CadCommand: Send {
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether a bare dynamic angle inherits its sign from the world-XY
|
||||
/// cursor side. Commands with their own 3D axis semantics opt out.
|
||||
fn dyn_auto_sign_angle(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Explicit per-step dynamic-input description. `Some(spec)` takes full
|
||||
/// control of the boxes, guide geometry and anchor for this step; `None`
|
||||
/// (the default) falls back to the legacy `dyn_field()` behaviour so
|
||||
|
||||
@@ -283,6 +283,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Angle = 0.0000° (degenerate rays)" => Some(("inquiry", "angle-0-0000-degenerate-rays")),
|
||||
"Angle format" => Some(("properties", "angle-format")),
|
||||
"Angle precision" => Some(("properties", "angle-precision")),
|
||||
"Angle of revolution" => Some(("properties", "angle-of-revolution")),
|
||||
"Angles" => Some(("model", "angles")),
|
||||
"Angular" => Some(("annotate", "angular")),
|
||||
"Angular decimals (DIMADEC)" => Some(("common", "angular-decimals-dimadec")),
|
||||
@@ -357,6 +358,12 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Axis Point" => Some(("model", "axis-point")),
|
||||
"Axis Vector" => Some(("model", "axis-vector")),
|
||||
"Axis, End" => Some(("draw", "axis-end")),
|
||||
"Axis direction X" => Some(("properties", "axis-direction-x")),
|
||||
"Axis direction Y" => Some(("properties", "axis-direction-y")),
|
||||
"Axis direction Z" => Some(("properties", "axis-direction-z")),
|
||||
"Axis position X" => Some(("properties", "axis-position-x")),
|
||||
"Axis position Y" => Some(("properties", "axis-position-y")),
|
||||
"Axis position Z" => Some(("properties", "axis-position-z")),
|
||||
"BACKGROUND colour [Default/Black/DarkGray/Gray/LightGray/White] or R G B (0–255):" => Some(("file", "background-colour-default-black-darkgray-gray")),
|
||||
"BASE Specify base point:" => Some(("insert", "base-specify-base-point")),
|
||||
"BEDIT Select block reference to edit:" => Some(("modify", "bedit-select-block-reference-to-edit")),
|
||||
@@ -2177,9 +2184,18 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"REVOLVE Angle of revolution <360>:" => Some(("insert", "revolve-angle-of-revolution-360")),
|
||||
"REVOLVE Axis end point:" => Some(("insert", "revolve-axis-end-point")),
|
||||
"REVOLVE Axis start point:" => Some(("insert", "revolve-axis-start-point")),
|
||||
"REVOLVE Creation mode [Solid/Surface]" => Some(("insert", "revolve-creation-mode-solid-surface")),
|
||||
"REVOLVE Enter angle expression:" => Some(("insert", "revolve-enter-angle-expression")),
|
||||
"REVOLVE Select line, ray, or construction line for axis:" => Some(("insert", "revolve-select-line-ray-or-construction-line-for-axis")),
|
||||
"REVOLVE Select objects to revolve or [Mode] (Enter to finish):" => Some(("insert", "revolve-select-objects-or-mode")),
|
||||
"REVOLVE Select profile:" => Some(("insert", "revolve-select-profile")),
|
||||
"REVOLVE Specify angle of revolution or [Start angle/Reverse/Expression]" => Some(("insert", "revolve-specify-angle-or-options")),
|
||||
"REVOLVE Specify axis start point or [Object/X/Y/Z/Mode]:" => Some(("insert", "revolve-specify-axis-start-or-options")),
|
||||
"REVOLVE Specify start angle" => Some(("insert", "revolve-specify-start-angle")),
|
||||
"REVOLVE: created %{created} object(s); %{failed} source(s) could not be revolved." => Some(("common", "revolve-created-objects-failed-sources")),
|
||||
"REVOLVE: could not revolve profile." => Some(("common", "revolve-could-not-revolve-profile")),
|
||||
"REVOLVE: entity not found." => Some(("common", "revolve-entity-not-found")),
|
||||
"REVOLVE: no selected object could be revolved with the requested options." => Some(("common", "revolve-no-selected-object-could-be-revolved")),
|
||||
"REVOLVE: solid created ({:.0}°)." => Some(("common", "revolve-solid-created-0")),
|
||||
"ROTATE Specify first reference point or type reference angle:" => Some(("modify", "rotate-specify-first-reference-point-or-type-r")),
|
||||
"ROTATE Specify new absolute angle [ref=%{a}]:" => Some(("modify", "rotate-specify-new-absolute-angle-ref-a")),
|
||||
@@ -2477,6 +2493,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Show Annotation Objects" => Some(("ui", "show-annotation-objects")),
|
||||
"Show Clipping" => Some(("properties", "show-clipping")),
|
||||
"Show Cropping" => Some(("properties", "show-cropping")),
|
||||
"Show History" => Some(("properties", "show-history")),
|
||||
"Show Intensity" => Some(("properties", "show-intensity")),
|
||||
"Show Lineweight" => Some(("ui", "show-lineweight")),
|
||||
"Show Lineweight\nLWDISPLAY" => Some(("ui", "show-lineweight-lwdisplay")),
|
||||
@@ -2494,6 +2511,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Snap to\nUnderlays" => Some(("insert", "snap-to-underlays")),
|
||||
"Softness" => Some(("properties", "softness")),
|
||||
"Solid" => Some(("properties", "solid")),
|
||||
"Solid History" => Some(("properties", "solid-history")),
|
||||
"Solid type" => Some(("properties", "solid-type")),
|
||||
"Solid created." => Some(("common", "solid-created")),
|
||||
"Solid3D" => Some(("properties", "solid3d")),
|
||||
"Source Files" => Some(("properties", "source-files")),
|
||||
|
||||
@@ -6,7 +6,7 @@ use acadrust::{entities::Solid3D, EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{
|
||||
CadCommand, CmdOption, CmdResult, ExtrudeExtent, ExtrudeMode, SelectionEntity,
|
||||
CadCommand, CmdOption, CmdResult, ExtrudeExtent, ExtrudeMode, SelectionEntity, WorkingPlane,
|
||||
};
|
||||
use crate::scene::WireModel;
|
||||
use crate::t;
|
||||
@@ -156,22 +156,28 @@ impl ExtrudeCommand {
|
||||
self.taper_angle,
|
||||
)
|
||||
};
|
||||
body.map(|body| preview_body_wires(&body, self.color))
|
||||
body.map(|body| preview_body_wires(&body, self.color, 0))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn preview_body_wires(body: &cadkernel::brep::Body, color: [f32; 4]) -> Vec<WireModel> {
|
||||
fn preview_body_wires(
|
||||
body: &cadkernel::brep::Body,
|
||||
color: [f32; 4],
|
||||
isolines: usize,
|
||||
) -> Vec<WireModel> {
|
||||
let mesh = cadkernel::brep::mesh::tessellate(
|
||||
body,
|
||||
cadkernel::brep::mesh::TessellationTolerance::new(
|
||||
cadkernel::tessellation::DEFAULT_ANGLE,
|
||||
1e-9,
|
||||
),
|
||||
)
|
||||
.with_isolines(isolines),
|
||||
);
|
||||
mesh.edges
|
||||
let mut wires = mesh
|
||||
.edges
|
||||
.into_iter()
|
||||
.filter(|edge| edge.positions.len() >= 2)
|
||||
.map(|edge| {
|
||||
@@ -182,7 +188,21 @@ fn preview_body_wires(body: &cadkernel::brep::Body, color: [f32; 4]) -> Vec<Wire
|
||||
false,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
.collect::<Vec<_>>();
|
||||
wires.extend(
|
||||
mesh.isolines
|
||||
.into_iter()
|
||||
.filter(|line| line.positions.len() >= 2)
|
||||
.map(|line| {
|
||||
WireModel::solid_f64(
|
||||
"REVOLVE-PREVIEW-ISOLINE".to_owned(),
|
||||
line.positions,
|
||||
color,
|
||||
false,
|
||||
)
|
||||
}),
|
||||
);
|
||||
wires
|
||||
}
|
||||
|
||||
impl CadCommand for ExtrudeCommand {
|
||||
@@ -615,30 +635,236 @@ impl CadCommand for PresspullCommand {
|
||||
|
||||
pub struct RevolveCommand {
|
||||
step: RevolveStep,
|
||||
target_handle: acadrust::Handle,
|
||||
handles: Vec<Handle>,
|
||||
preview_profiles: Vec<(Handle, EntityType)>,
|
||||
injected_axis: Option<EntityType>,
|
||||
axis_start: DVec3,
|
||||
axis_end: DVec3,
|
||||
working_plane: WorkingPlane,
|
||||
mode: ExtrudeMode,
|
||||
mode_return: RevolveStep,
|
||||
angle: f64,
|
||||
start_angle: f64,
|
||||
reverse: bool,
|
||||
isolines: usize,
|
||||
color: [f32; 4],
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct RevolveDefaults {
|
||||
mode: ExtrudeMode,
|
||||
angle: f64,
|
||||
start_angle: f64,
|
||||
}
|
||||
|
||||
fn revolve_defaults() -> &'static Mutex<RevolveDefaults> {
|
||||
static DEFAULTS: OnceLock<Mutex<RevolveDefaults>> = OnceLock::new();
|
||||
DEFAULTS.get_or_init(|| {
|
||||
Mutex::new(RevolveDefaults {
|
||||
mode: ExtrudeMode::Solid,
|
||||
angle: std::f64::consts::TAU,
|
||||
start_angle: 0.0,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum RevolveStep {
|
||||
Pick,
|
||||
Mode,
|
||||
AxisStart,
|
||||
AxisEnd,
|
||||
AxisObject,
|
||||
Angle,
|
||||
StartAngle,
|
||||
Expression,
|
||||
}
|
||||
|
||||
impl RevolveCommand {
|
||||
pub fn new(color: [f32; 4]) -> Self {
|
||||
pub fn new(color: [f32; 4], isolines: usize) -> Self {
|
||||
let defaults = *revolve_defaults().lock().unwrap_or_else(|error| error.into_inner());
|
||||
Self {
|
||||
step: RevolveStep::Pick,
|
||||
target_handle: acadrust::Handle::NULL,
|
||||
handles: Vec::new(),
|
||||
preview_profiles: Vec::new(),
|
||||
injected_axis: None,
|
||||
axis_start: DVec3::ZERO,
|
||||
axis_end: DVec3::new(0.0, 0.0, 1.0),
|
||||
working_plane: WorkingPlane::default(),
|
||||
mode: defaults.mode,
|
||||
mode_return: RevolveStep::Pick,
|
||||
angle: defaults.angle,
|
||||
start_angle: defaults.start_angle,
|
||||
reverse: false,
|
||||
isolines,
|
||||
color,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_preselection(&mut self, profiles: Vec<(Handle, EntityType)>) {
|
||||
let selected_handles = profiles.iter().map(|(handle, _)| *handle).collect::<Vec<_>>();
|
||||
self.preview_profiles = profiles
|
||||
.into_iter()
|
||||
.filter(|(_, entity)| {
|
||||
crate::scene::model::sweep_model::extrusion_profile_of(entity).is_some()
|
||||
})
|
||||
.collect();
|
||||
self.handles = if self.preview_profiles.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
selected_handles
|
||||
};
|
||||
if !self.handles.is_empty() {
|
||||
self.step = RevolveStep::AxisStart;
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_reference(&self) -> Option<DVec3> {
|
||||
let axis = (self.axis_end - self.axis_start).try_normalize()?;
|
||||
self.preview_profiles
|
||||
.iter()
|
||||
.filter_map(|(_, entity)| {
|
||||
crate::scene::model::sweep_model::extrusion_profile_of(entity)
|
||||
})
|
||||
.flat_map(|(profile, _)| {
|
||||
profile.pieces.into_iter().flat_map(move |piece| {
|
||||
[0.0, 0.5, 1.0].into_iter().map(move |parameter| {
|
||||
DVec3::from_array(profile.plane.point_at(piece.point_at(parameter)))
|
||||
})
|
||||
})
|
||||
})
|
||||
.filter(|point| point.is_finite())
|
||||
.max_by(|first, second| {
|
||||
let radial_length = |point: DVec3| {
|
||||
let delta = point - self.axis_start;
|
||||
(delta - axis * delta.dot(axis)).length_squared()
|
||||
};
|
||||
radial_length(*first).total_cmp(&radial_length(*second))
|
||||
})
|
||||
}
|
||||
|
||||
fn angle_anchor(&self) -> Option<DVec3> {
|
||||
let axis = (self.axis_end - self.axis_start).try_normalize()?;
|
||||
let reference = self.profile_reference()?;
|
||||
Some(self.axis_start + axis * (reference - self.axis_start).dot(axis))
|
||||
}
|
||||
|
||||
fn cursor_angle(&self, cursor: DVec3, full_at_zero: bool) -> Option<f64> {
|
||||
let axis = (self.axis_end - self.axis_start).try_normalize()?;
|
||||
let anchor = self.angle_anchor()?;
|
||||
let reference = (self.profile_reference()? - anchor).try_normalize()?;
|
||||
let cursor_direction = cursor - anchor;
|
||||
let radial = (cursor_direction - axis * cursor_direction.dot(axis)).try_normalize()?;
|
||||
let mut angle = axis
|
||||
.dot(reference.cross(radial))
|
||||
.atan2(reference.dot(radial));
|
||||
if angle < 0.0 {
|
||||
angle += std::f64::consts::TAU;
|
||||
}
|
||||
if full_at_zero && angle.abs() <= 1e-9 {
|
||||
angle = std::f64::consts::TAU;
|
||||
}
|
||||
angle.is_finite().then_some(angle)
|
||||
}
|
||||
|
||||
fn signed_angle(&self, angle: f64) -> f64 {
|
||||
angle * if self.reverse { -1.0 } else { 1.0 }
|
||||
}
|
||||
|
||||
fn preview_wires(&self, cursor: DVec3) -> Vec<WireModel> {
|
||||
let (angle, start_angle) = match self.step {
|
||||
RevolveStep::Angle => (
|
||||
self.cursor_angle(cursor, true)
|
||||
.map(|angle| self.signed_angle(angle)),
|
||||
Some(self.start_angle),
|
||||
),
|
||||
RevolveStep::StartAngle => (
|
||||
Some(self.signed_angle(self.angle)),
|
||||
self.cursor_angle(cursor, false),
|
||||
),
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
let (Some(angle), Some(start_angle)) = (angle, start_angle) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let from = self.axis_start.to_array();
|
||||
let to = self.axis_end.to_array();
|
||||
self.preview_profiles
|
||||
.iter()
|
||||
.flat_map(|(_, entity)| {
|
||||
let Some((_, closed)) =
|
||||
crate::scene::model::sweep_model::extrusion_profile_of(entity)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let creates_surface = self.mode == ExtrudeMode::Surface || !closed;
|
||||
let body = if creates_surface {
|
||||
crate::scene::model::sweep_model::revolved_surface(
|
||||
entity,
|
||||
from,
|
||||
to,
|
||||
angle,
|
||||
start_angle,
|
||||
)
|
||||
} else {
|
||||
crate::scene::model::sweep_model::revolved(
|
||||
entity,
|
||||
from,
|
||||
to,
|
||||
angle,
|
||||
start_angle,
|
||||
)
|
||||
};
|
||||
body.map(|body| preview_body_wires(&body, self.color, self.isolines))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn set_axis(&mut self, direction: DVec3) -> CmdResult {
|
||||
let Some(direction) = direction.try_normalize() else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
self.axis_start = self.working_plane.origin;
|
||||
self.axis_end = self.axis_start + direction;
|
||||
self.step = RevolveStep::Angle;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn finish(&self, angle: f64) -> CmdResult {
|
||||
let angle = self.signed_angle(angle);
|
||||
if angle.is_finite() && angle.abs() > 1e-9 && angle.abs() <= std::f64::consts::TAU + 1e-9 {
|
||||
let mut defaults = revolve_defaults().lock().unwrap_or_else(|error| error.into_inner());
|
||||
defaults.mode = self.mode;
|
||||
defaults.angle = angle.abs();
|
||||
defaults.start_angle = self.start_angle;
|
||||
}
|
||||
CmdResult::RevolveEntities {
|
||||
handles: self.handles.clone(),
|
||||
axis_start: self.axis_start,
|
||||
axis_end: self.axis_end,
|
||||
angle,
|
||||
start_angle: self.start_angle,
|
||||
mode: self.mode,
|
||||
color: self.color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn revolve_axis(entity: &EntityType) -> Option<(DVec3, DVec3)> {
|
||||
let point = |value: &acadrust::types::Vector3| DVec3::new(value.x, value.y, value.z);
|
||||
let (start, direction) = match entity {
|
||||
EntityType::Line(line) => (point(&line.start), point(&line.end) - point(&line.start)),
|
||||
EntityType::Ray(ray) => (point(&ray.base_point), point(&ray.direction)),
|
||||
EntityType::XLine(line) => (point(&line.base_point), point(&line.direction)),
|
||||
EntityType::Polyline3D(line) if line.vertices.len() == 2 => {
|
||||
let start = point(&line.vertices[0].position);
|
||||
(start, point(&line.vertices[1].position) - start)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let direction = direction.try_normalize()?;
|
||||
Some((start, start + direction))
|
||||
}
|
||||
|
||||
impl CadCommand for RevolveCommand {
|
||||
@@ -647,22 +873,70 @@ impl CadCommand for RevolveCommand {
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
RevolveStep::Pick => t!("REVOLVE Select profile:").into_owned(),
|
||||
RevolveStep::AxisStart => t!("REVOLVE Axis start point:").into_owned(),
|
||||
RevolveStep::Pick => t!("REVOLVE Select objects to revolve or [Mode] (Enter to finish):").into_owned(),
|
||||
RevolveStep::Mode => format!(
|
||||
"{} <{}>:",
|
||||
t!("REVOLVE Creation mode [Solid/Surface]"),
|
||||
if self.mode == ExtrudeMode::Solid { "Solid" } else { "Surface" }
|
||||
),
|
||||
RevolveStep::AxisStart => t!("REVOLVE Specify axis start point or [Object/X/Y/Z/Mode]:").into_owned(),
|
||||
RevolveStep::AxisEnd => t!("REVOLVE Axis end point:").into_owned(),
|
||||
RevolveStep::Angle => t!("REVOLVE Angle of revolution <360>:").into_owned(),
|
||||
RevolveStep::AxisObject => t!("REVOLVE Select line, ray, or construction line for axis:").into_owned(),
|
||||
RevolveStep::Angle => format!(
|
||||
"{} <{}>:",
|
||||
t!("REVOLVE Specify angle of revolution or [Start angle/Reverse/Expression]"),
|
||||
crate::entities::common::format_angle(self.angle)
|
||||
),
|
||||
RevolveStep::StartAngle => format!(
|
||||
"{} <{}>:",
|
||||
t!("REVOLVE Specify start angle"),
|
||||
crate::entities::common::format_angle(self.start_angle)
|
||||
),
|
||||
RevolveStep::Expression => t!("REVOLVE Enter angle expression:").into_owned(),
|
||||
}
|
||||
}
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
RevolveStep::Pick => vec![CmdOption::new("Mode", "MODE"), CmdOption::enter("Done")],
|
||||
RevolveStep::Mode => vec![
|
||||
CmdOption::new("Solid", "SOLID"),
|
||||
CmdOption::new("Surface", "SURFACE"),
|
||||
],
|
||||
RevolveStep::AxisStart => vec![
|
||||
CmdOption::new("Object", "OBJECT"),
|
||||
CmdOption::new("X", "X"),
|
||||
CmdOption::new("Y", "Y"),
|
||||
CmdOption::new("Z", "Z"),
|
||||
CmdOption::new("Mode", "MODE"),
|
||||
],
|
||||
RevolveStep::Angle => vec![
|
||||
CmdOption::new("Start angle", "START"),
|
||||
CmdOption::new("Reverse", "REVERSE"),
|
||||
CmdOption::new("Expression", "EXPRESSION"),
|
||||
],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.step == RevolveStep::Pick
|
||||
self.step == RevolveStep::AxisObject
|
||||
}
|
||||
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.target_handle = handle;
|
||||
self.step = RevolveStep::AxisStart;
|
||||
CmdResult::NeedPoint
|
||||
match self.step {
|
||||
RevolveStep::AxisObject => {
|
||||
let Some((start, end)) = self.injected_axis.take().as_ref().and_then(revolve_axis)
|
||||
else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
self.axis_start = start;
|
||||
self.axis_end = end;
|
||||
self.step = RevolveStep::Angle;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
@@ -672,46 +946,235 @@ impl CadCommand for RevolveCommand {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
RevolveStep::AxisEnd => {
|
||||
self.axis_end = pt;
|
||||
if (pt - self.axis_start).length_squared() <= 1e-12 {
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
self.axis_end = pt;
|
||||
self.step = RevolveStep::Angle;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
RevolveStep::Angle => self
|
||||
.cursor_angle(pt, true)
|
||||
.map(|angle| self.finish(angle))
|
||||
.unwrap_or(CmdResult::NeedPoint),
|
||||
RevolveStep::StartAngle => {
|
||||
let Some(angle) = self.cursor_angle(pt, false) else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
self.start_angle = angle;
|
||||
self.step = RevolveStep::Angle;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
RevolveStep::Angle => self.make_revolve(360.0),
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
fn wants_text_input(&self) -> bool {
|
||||
self.step == RevolveStep::Angle
|
||||
matches!(
|
||||
self.step,
|
||||
RevolveStep::Pick
|
||||
| RevolveStep::Mode
|
||||
| RevolveStep::AxisStart
|
||||
| RevolveStep::Angle
|
||||
| RevolveStep::StartAngle
|
||||
| RevolveStep::Expression
|
||||
)
|
||||
}
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(self.step, RevolveStep::AxisStart | RevolveStep::Angle)
|
||||
}
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let angle = if text.trim().is_empty() {
|
||||
360.0f32
|
||||
} else {
|
||||
text.trim()
|
||||
.parse::<f32>()
|
||||
.ok()
|
||||
.filter(|&a| a.abs() > 1e-3)?
|
||||
};
|
||||
Some(self.make_revolve(angle))
|
||||
let value = text.trim();
|
||||
if value.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let upper = value.to_ascii_uppercase();
|
||||
match self.step {
|
||||
RevolveStep::Pick if "MODE".starts_with(&upper) => {
|
||||
self.mode_return = RevolveStep::Pick;
|
||||
self.step = RevolveStep::Mode;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
RevolveStep::Mode => {
|
||||
if "SOLID".starts_with(&upper) {
|
||||
self.mode = ExtrudeMode::Solid;
|
||||
} else if "SURFACE".starts_with(&upper) {
|
||||
self.mode = ExtrudeMode::Surface;
|
||||
} else {
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
revolve_defaults()
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner())
|
||||
.mode = self.mode;
|
||||
self.step = self.mode_return;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
RevolveStep::AxisStart => {
|
||||
if "OBJECT".starts_with(&upper) {
|
||||
self.step = RevolveStep::AxisObject;
|
||||
Some(CmdResult::NeedPoint)
|
||||
} else if "MODE".starts_with(&upper) {
|
||||
self.mode_return = RevolveStep::AxisStart;
|
||||
self.step = RevolveStep::Mode;
|
||||
Some(CmdResult::NeedPoint)
|
||||
} else if upper == "X" {
|
||||
Some(self.set_axis(self.working_plane.x))
|
||||
} else if upper == "Y" {
|
||||
Some(self.set_axis(self.working_plane.y))
|
||||
} else if upper == "Z" {
|
||||
Some(self.set_axis(self.working_plane.z))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
RevolveStep::Angle => {
|
||||
if "START ANGLE".starts_with(&upper) || "START".starts_with(&upper) {
|
||||
self.step = RevolveStep::StartAngle;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if "REVERSE".starts_with(&upper) {
|
||||
self.reverse = !self.reverse;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if "EXPRESSION".starts_with(&upper) {
|
||||
self.step = RevolveStep::Expression;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
crate::entities::common::parse_angle(value)
|
||||
.filter(|angle| {
|
||||
angle.is_finite()
|
||||
&& angle.abs() > 1e-9
|
||||
&& angle.abs() <= std::f64::consts::TAU + 1e-9
|
||||
})
|
||||
.map(|angle| self.finish(angle))
|
||||
}
|
||||
RevolveStep::StartAngle => {
|
||||
let angle = crate::entities::common::parse_angle(value)?;
|
||||
if !angle.is_finite() {
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
self.start_angle = angle;
|
||||
self.step = RevolveStep::Angle;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
RevolveStep::Expression => {
|
||||
let angle = crate::app::expr_eval::eval_number(value)
|
||||
.and_then(|value| crate::entities::common::parse_angle(&value.to_string()))?;
|
||||
if !angle.is_finite()
|
||||
|| angle.abs() <= 1e-9
|
||||
|| angle.abs() > std::f64::consts::TAU + 1e-9
|
||||
{
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
Some(self.finish(angle))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.step == RevolveStep::Angle {
|
||||
self.make_revolve(360.0)
|
||||
} else {
|
||||
CmdResult::Cancel
|
||||
match self.step {
|
||||
RevolveStep::Pick if !self.handles.is_empty() => {
|
||||
self.step = RevolveStep::AxisStart;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
RevolveStep::Mode => {
|
||||
self.step = self.mode_return;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
RevolveStep::Angle => self.finish(self.angle),
|
||||
RevolveStep::StartAngle => {
|
||||
self.step = RevolveStep::Angle;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RevolveCommand {
|
||||
fn make_revolve(&self, angle_deg: f32) -> CmdResult {
|
||||
CmdResult::RevolveEntity {
|
||||
handle: self.target_handle,
|
||||
axis_start: self.axis_start,
|
||||
axis_end: self.axis_end,
|
||||
angle_deg,
|
||||
color: self.color,
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.working_plane = plane;
|
||||
}
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
self.step == RevolveStep::Pick
|
||||
}
|
||||
fn selection_forces_add(&self) -> bool {
|
||||
self.step == RevolveStep::Pick
|
||||
}
|
||||
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
|
||||
if self.step != RevolveStep::Pick {
|
||||
return;
|
||||
}
|
||||
let selected_handles = entities.iter().map(|entry| entry.handle).collect::<Vec<_>>();
|
||||
self.preview_profiles = entities
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
crate::scene::model::sweep_model::extrusion_profile_of(&entry.entity).is_some()
|
||||
})
|
||||
.map(|entry| (entry.handle, entry.entity))
|
||||
.collect();
|
||||
self.handles = if self.preview_profiles.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
selected_handles
|
||||
};
|
||||
}
|
||||
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
self.step == RevolveStep::AxisObject
|
||||
}
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
if self.step == RevolveStep::AxisObject {
|
||||
self.injected_axis = Some(entity);
|
||||
}
|
||||
}
|
||||
fn on_preview_wires(&mut self, cursor: DVec3) -> Vec<WireModel> {
|
||||
self.preview_wires(cursor)
|
||||
}
|
||||
fn dyn_field(&self) -> crate::command::DynField {
|
||||
if matches!(self.step, RevolveStep::Angle | RevolveStep::StartAngle) {
|
||||
crate::command::DynField::Angle
|
||||
} else {
|
||||
crate::command::DynField::Point
|
||||
}
|
||||
}
|
||||
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
|
||||
matches!(self.step, RevolveStep::Angle | RevolveStep::StartAngle).then(|| {
|
||||
crate::command::DynSpec {
|
||||
anchor: crate::command::DynAnchor::Point(
|
||||
self.angle_anchor().unwrap_or(self.axis_start),
|
||||
),
|
||||
fields: vec![crate::command::DynFieldSpec::new(crate::command::DynRole::Angle)],
|
||||
guide: crate::command::DynGuide::None,
|
||||
ref_point: self.profile_reference(),
|
||||
}
|
||||
})
|
||||
}
|
||||
fn dyn_commit_as_text(&self) -> bool {
|
||||
matches!(self.step, RevolveStep::Angle | RevolveStep::StartAngle)
|
||||
}
|
||||
fn dyn_auto_sign_angle(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn cursor_plane(&self) -> Option<(DVec3, DVec3)> {
|
||||
if !matches!(self.step, RevolveStep::Angle | RevolveStep::StartAngle) {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
(self.axis_end - self.axis_start).try_normalize()?,
|
||||
self.angle_anchor()?,
|
||||
))
|
||||
}
|
||||
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||
let full_at_zero = self.step == RevolveStep::Angle;
|
||||
self.cursor_angle(cursor, full_at_zero).map(|angle| {
|
||||
let angle = if self.step == RevolveStep::Angle {
|
||||
self.signed_angle(angle)
|
||||
} else {
|
||||
angle
|
||||
};
|
||||
crate::command::dyn_display_angle_deg(angle as f32) as f64
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,10 +1331,79 @@ pub fn empty_extruded_surface(direction: DVec3, taper_angle: f64) -> EntityType
|
||||
EntityType::Surface(surface)
|
||||
}
|
||||
|
||||
pub fn empty_revolved_surface(
|
||||
profile: &EntityType,
|
||||
axis_start: DVec3,
|
||||
axis_end: DVec3,
|
||||
angle: f64,
|
||||
start_angle: f64,
|
||||
) -> EntityType {
|
||||
use acadrust::entities::{Surface, SurfaceData, SurfaceKind};
|
||||
use acadrust::types::Vector3;
|
||||
|
||||
let mut surface = Surface::new(SurfaceKind::Revolved);
|
||||
let axis = (axis_end - axis_start).normalize_or(DVec3::Z);
|
||||
surface.point_of_reference = Vector3::new(axis_start.x, axis_start.y, axis_start.z);
|
||||
if let SurfaceData::Revolved {
|
||||
revolve_entity,
|
||||
axis_point,
|
||||
axis_vector,
|
||||
revolve_angle,
|
||||
start_angle: stored_start,
|
||||
entity_transform,
|
||||
solid,
|
||||
..
|
||||
} = &mut surface.surface_data
|
||||
{
|
||||
if let Some((embedded, transform)) =
|
||||
crate::scene::model::sweep_model::embedded_revolve_profile(profile)
|
||||
{
|
||||
*revolve_entity = Some(embedded);
|
||||
*entity_transform = transform;
|
||||
}
|
||||
*axis_point = Vector3::new(axis_start.x, axis_start.y, axis_start.z);
|
||||
*axis_vector = Vector3::new(axis.x, axis.y, axis.z);
|
||||
*revolve_angle = angle;
|
||||
*stored_start = start_angle;
|
||||
*solid = false;
|
||||
}
|
||||
EntityType::Surface(surface)
|
||||
}
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["EXTRUDE", "THICKEN", "PRESSPULL"]
|
||||
});
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["LOFT"] });
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["REVOLVE"] });
|
||||
|
||||
#[cfg(test)]
|
||||
mod revolve_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cursor_angle_is_measured_about_the_model_axis() {
|
||||
let profile = EntityType::Line(acadrust::entities::Line::from_coords(
|
||||
2.0, 0.0, 0.0, 2.0, 4.0, 0.0,
|
||||
));
|
||||
let mut command = RevolveCommand::new([1.0; 4], 0);
|
||||
command.set_preselection(vec![(Handle::new(1), profile)]);
|
||||
command.axis_start = DVec3::ZERO;
|
||||
command.axis_end = DVec3::Y;
|
||||
command.step = RevolveStep::Angle;
|
||||
|
||||
let anchor = command.angle_anchor().unwrap();
|
||||
let quarter = anchor - DVec3::Z * 2.0;
|
||||
let angle = command.cursor_angle(quarter, false).unwrap();
|
||||
assert!((angle - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
|
||||
|
||||
let displaced_along_axis = quarter + DVec3::Y * 100.0;
|
||||
let angle = command.cursor_angle(displaced_along_axis, false).unwrap();
|
||||
assert!((angle - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
|
||||
|
||||
let (normal, origin) = command.cursor_plane().unwrap();
|
||||
assert!((normal - DVec3::Y).length_squared() < 1e-24);
|
||||
assert!((origin - anchor).length_squared() < 1e-24);
|
||||
}
|
||||
}
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["SWEEP"] });
|
||||
+21
-4
@@ -2729,14 +2729,28 @@ impl Scene {
|
||||
}
|
||||
|
||||
/// Cache and display a Model-tab kernel solid.
|
||||
pub fn register_solid_model(&mut self, handle: Handle, solid: cadkernel::brep::Body) {
|
||||
pub fn register_solid_model(
|
||||
&mut self,
|
||||
handle: Handle,
|
||||
solid: cadkernel::brep::Body,
|
||||
) -> bool {
|
||||
let color = self
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.map(|e| self.render_style(e).0)
|
||||
.unwrap_or([0.8, 0.8, 0.85, 1.0]);
|
||||
if let Some((mut set, wires, center)) =
|
||||
crate::scene::model::solid_model::display_from_solid(&solid, color)
|
||||
let facet_resolution = self.document.header.facet_resolution;
|
||||
let chordal_deflection =
|
||||
crate::entities::solid3d::display_deflection(&self.document.header, facet_resolution);
|
||||
let isolines = self.document.header.isolines.max(0) as usize;
|
||||
let displayed = if let Some((mut set, wires, center)) =
|
||||
crate::scene::model::solid_model::display_from_solid(
|
||||
&solid,
|
||||
color,
|
||||
facet_resolution,
|
||||
chordal_deflection,
|
||||
isolines,
|
||||
)
|
||||
{
|
||||
if let Some(entity) = self.document.get_entity_mut(handle) {
|
||||
let reference = acadrust::types::Vector3::new(center[0], center[1], center[2]);
|
||||
@@ -2778,15 +2792,18 @@ impl Scene {
|
||||
);
|
||||
}
|
||||
self.meshes.insert(handle, set);
|
||||
true
|
||||
} else {
|
||||
self.meshes.remove(&handle);
|
||||
}
|
||||
false
|
||||
};
|
||||
self.solid_models.insert(handle, solid);
|
||||
self.sync_solid_reference_point(handle);
|
||||
// Only this solid's mesh changed — report just its handle so the mesh /
|
||||
// wire caches patch it in rather than rebuilding the whole drawing (and
|
||||
// so a bulk register loop stays O(n), not O(n²)).
|
||||
self.bump_entities(&[(handle, ChangeKind::Modified)]);
|
||||
displayed
|
||||
}
|
||||
|
||||
/// `BACKGROUND` change picks up the new `adapt_to_bg` result without
|
||||
|
||||
@@ -68,6 +68,13 @@ pub const PROP_SCALE_ALONG_PATH: &str = "solid_history_scale_along_path";
|
||||
pub const PROP_SWEEP_LENGTH: &str = "solid_history_sweep_length";
|
||||
pub const PROP_EXTRUSION_HEIGHT: &str = "solid_history_extrusion_height";
|
||||
pub const PROP_TAPER_ANGLE: &str = "solid_history_taper_angle";
|
||||
pub const PROP_REVOLVE_ANGLE: &str = "solid_history_revolve_angle";
|
||||
pub const PROP_AXIS_POSITION_X: &str = "solid_history_axis_position_x";
|
||||
pub const PROP_AXIS_POSITION_Y: &str = "solid_history_axis_position_y";
|
||||
pub const PROP_AXIS_POSITION_Z: &str = "solid_history_axis_position_z";
|
||||
pub const PROP_AXIS_DIRECTION_X: &str = "solid_history_axis_direction_x";
|
||||
pub const PROP_AXIS_DIRECTION_Y: &str = "solid_history_axis_direction_y";
|
||||
pub const PROP_AXIS_DIRECTION_Z: &str = "solid_history_axis_direction_z";
|
||||
pub const PROP_PYRAMID_TYPE: &str = "solid_history_pyramid_type";
|
||||
pub const PROP_HISTORY: &str = "solid_history_record";
|
||||
pub const PROP_SHOW_HISTORY: &str = "solid_history_show";
|
||||
@@ -106,6 +113,73 @@ fn displayed_history_state(object_show_history: bool, show_history_mode: i16) ->
|
||||
}
|
||||
}
|
||||
|
||||
fn revolve_profile_world_normal(value: &SolidHistoryRevolve) -> Option<glam::DVec3> {
|
||||
let profile = value.sweep_entity.as_ref().and_then(embedded_entity)?;
|
||||
let normal = crate::entities::curve::entity_curve(&profile)?.plane.normal()?;
|
||||
let inverse_transpose = matrix(value.base.transform)?.inverse().transpose();
|
||||
inverse_transpose
|
||||
.transform_vector3(glam::DVec3::from_array(normal))
|
||||
.try_normalize()
|
||||
}
|
||||
|
||||
fn revolve_position_component_editable(value: &SolidHistoryRevolve, axis: usize) -> bool {
|
||||
revolve_profile_world_normal(value).is_some_and(|normal| normal[axis].abs() <= 1e-9)
|
||||
}
|
||||
|
||||
fn revolve_direction_component_capacity(
|
||||
value: &SolidHistoryRevolve,
|
||||
axis: usize,
|
||||
) -> Option<(glam::DVec3, f64)> {
|
||||
let normal = revolve_profile_world_normal(value)?;
|
||||
let capacity = (1.0 - normal[axis] * normal[axis]).max(0.0).sqrt();
|
||||
Some((normal, capacity))
|
||||
}
|
||||
|
||||
fn revolve_direction_component_editable(value: &SolidHistoryRevolve, axis: usize) -> bool {
|
||||
revolve_direction_component_capacity(value, axis)
|
||||
.is_some_and(|(_, capacity)| capacity > 1e-9)
|
||||
}
|
||||
|
||||
fn revolve_position_property(
|
||||
value: &SolidHistoryRevolve,
|
||||
axis: usize,
|
||||
label: &str,
|
||||
field: &'static str,
|
||||
position: f64,
|
||||
) -> Property {
|
||||
if revolve_position_component_editable(value, axis) {
|
||||
crate::entities::common::edit_prop(label, field, position)
|
||||
} else {
|
||||
Property {
|
||||
label: label.to_string(),
|
||||
field,
|
||||
value: PropValue::ReadOnly(crate::entities::common::format_length(position)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn revolve_direction_property(
|
||||
value: &SolidHistoryRevolve,
|
||||
axis: usize,
|
||||
label: &str,
|
||||
field: &'static str,
|
||||
direction: f64,
|
||||
) -> Property {
|
||||
if revolve_direction_component_editable(value, axis) {
|
||||
crate::entities::common::edit_scalar_prop(label, field, direction)
|
||||
} else {
|
||||
Property {
|
||||
label: label.to_string(),
|
||||
field,
|
||||
value: PropValue::ReadOnly(if direction == 0.0 {
|
||||
"0".to_string()
|
||||
} else {
|
||||
direction.to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_specialized_primitive_properties(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
@@ -122,6 +196,7 @@ pub fn has_specialized_primitive_properties(
|
||||
| SolidHistoryOperation::Pyramid(_)
|
||||
| SolidHistoryOperation::Sweep(_)
|
||||
| SolidHistoryOperation::Extrusion(_)
|
||||
| SolidHistoryOperation::Revolve(_)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -147,10 +222,124 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3>
|
||||
),
|
||||
SolidHistoryOperation::Torus(value) => world_point(value.base.transform, [0.0; 3]),
|
||||
SolidHistoryOperation::Pyramid(value) => world_point(value.base.transform, [0.0; 3]),
|
||||
SolidHistoryOperation::Revolve(value) => world_point(
|
||||
value.base.transform,
|
||||
[value.axis_point.x, value.axis_point.y, value.axis_point.z],
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn revolve_properties(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
value: &SolidHistoryRevolve,
|
||||
) -> Vec<PropSection> {
|
||||
let Some(axis_position) = world_point(
|
||||
value.base.transform,
|
||||
[value.axis_point.x, value.axis_point.y, value.axis_point.z],
|
||||
) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(axis_direction) = world_vector(
|
||||
value.base.transform,
|
||||
[value.direction.x, value.direction.y, value.direction.z],
|
||||
) else {
|
||||
return Vec::new();
|
||||
};
|
||||
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 show_value = if show_history { "Yes" } else { "No" };
|
||||
vec![
|
||||
PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: vec![
|
||||
Property {
|
||||
label: t!("Solid type").into_owned(),
|
||||
field: "solid_history_type",
|
||||
value: PropValue::ReadOnly(t!("Revolve").into_owned()),
|
||||
},
|
||||
Property {
|
||||
label: t!("Angle of revolution").into_owned(),
|
||||
field: PROP_REVOLVE_ANGLE,
|
||||
value: PropValue::EditText(crate::entities::common::format_angle(
|
||||
value.revolve_angle,
|
||||
)),
|
||||
},
|
||||
revolve_position_property(
|
||||
value,
|
||||
0,
|
||||
t!("Axis position X").as_ref(),
|
||||
PROP_AXIS_POSITION_X,
|
||||
axis_position.x,
|
||||
),
|
||||
revolve_position_property(
|
||||
value,
|
||||
1,
|
||||
t!("Axis position Y").as_ref(),
|
||||
PROP_AXIS_POSITION_Y,
|
||||
axis_position.y,
|
||||
),
|
||||
revolve_position_property(
|
||||
value,
|
||||
2,
|
||||
t!("Axis position Z").as_ref(),
|
||||
PROP_AXIS_POSITION_Z,
|
||||
axis_position.z,
|
||||
),
|
||||
revolve_direction_property(
|
||||
value,
|
||||
0,
|
||||
t!("Axis direction X").as_ref(),
|
||||
PROP_AXIS_DIRECTION_X,
|
||||
axis_direction.x,
|
||||
),
|
||||
revolve_direction_property(
|
||||
value,
|
||||
1,
|
||||
t!("Axis direction Y").as_ref(),
|
||||
PROP_AXIS_DIRECTION_Y,
|
||||
axis_direction.y,
|
||||
),
|
||||
revolve_direction_property(
|
||||
value,
|
||||
2,
|
||||
t!("Axis direction Z").as_ref(),
|
||||
PROP_AXIS_DIRECTION_Z,
|
||||
axis_direction.z,
|
||||
),
|
||||
],
|
||||
},
|
||||
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: show_value.to_string(),
|
||||
options: vec!["No".to_string(), "Yes".to_string()],
|
||||
}
|
||||
} else {
|
||||
PropValue::ReadOnly(show_value.to_string())
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn extrusion_properties(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
@@ -1021,6 +1210,7 @@ pub fn primitive_properties(
|
||||
SolidHistoryOperation::Extrusion(value) => {
|
||||
extrusion_properties(document, handle, value)
|
||||
}
|
||||
SolidHistoryOperation::Revolve(value) => revolve_properties(document, handle, value),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -1053,6 +1243,13 @@ pub fn is_primitive_property(field: &str) -> bool {
|
||||
| PROP_SCALE_ALONG_PATH
|
||||
| PROP_EXTRUSION_HEIGHT
|
||||
| PROP_TAPER_ANGLE
|
||||
| PROP_REVOLVE_ANGLE
|
||||
| PROP_AXIS_POSITION_X
|
||||
| PROP_AXIS_POSITION_Y
|
||||
| PROP_AXIS_POSITION_Z
|
||||
| PROP_AXIS_DIRECTION_X
|
||||
| PROP_AXIS_DIRECTION_Y
|
||||
| PROP_AXIS_DIRECTION_Z
|
||||
| PROP_PYRAMID_TYPE
|
||||
)
|
||||
}
|
||||
@@ -1142,6 +1339,117 @@ fn apply_extrusion_geometry_property(
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_revolve_geometry_property(
|
||||
value: &mut SolidHistoryRevolve,
|
||||
field: &str,
|
||||
text: &str,
|
||||
) -> Option<bool> {
|
||||
if field == PROP_REVOLVE_ANGLE {
|
||||
let angle = crate::entities::common::parse_angle(text)?;
|
||||
if !angle.is_finite()
|
||||
|| angle.abs() <= 1e-12
|
||||
|| angle.abs() > std::f64::consts::TAU + 1e-12
|
||||
|| (angle - value.revolve_angle).abs() <= 1e-12
|
||||
{
|
||||
return Some(false);
|
||||
}
|
||||
value.revolve_angle = angle;
|
||||
return Some(true);
|
||||
}
|
||||
|
||||
if matches!(
|
||||
field,
|
||||
PROP_AXIS_POSITION_X | PROP_AXIS_POSITION_Y | PROP_AXIS_POSITION_Z
|
||||
) {
|
||||
let axis = match field {
|
||||
PROP_AXIS_POSITION_X => 0,
|
||||
PROP_AXIS_POSITION_Y => 1,
|
||||
_ => 2,
|
||||
};
|
||||
if !revolve_position_component_editable(value, axis) {
|
||||
return Some(false);
|
||||
}
|
||||
let target = crate::entities::common::parse_length(text)?;
|
||||
if !target.is_finite() {
|
||||
return Some(false);
|
||||
}
|
||||
let mut world = world_point(
|
||||
value.base.transform,
|
||||
[value.axis_point.x, value.axis_point.y, value.axis_point.z],
|
||||
)?;
|
||||
if (world[axis] - target).abs() <= 1e-12 {
|
||||
return Some(false);
|
||||
}
|
||||
world[axis] = target;
|
||||
let local = local_point(value.base.transform, world)?;
|
||||
if !local.is_finite() {
|
||||
return Some(false);
|
||||
}
|
||||
value.axis_point = acadrust::types::Vector3::new(local.x, local.y, local.z);
|
||||
return Some(true);
|
||||
}
|
||||
|
||||
if matches!(
|
||||
field,
|
||||
PROP_AXIS_DIRECTION_X | PROP_AXIS_DIRECTION_Y | PROP_AXIS_DIRECTION_Z
|
||||
) {
|
||||
let axis = match field {
|
||||
PROP_AXIS_DIRECTION_X => 0,
|
||||
PROP_AXIS_DIRECTION_Y => 1,
|
||||
_ => 2,
|
||||
};
|
||||
let target = text.trim().replace(',', ".").parse::<f64>().ok()?;
|
||||
let Some((normal, capacity)) = revolve_direction_component_capacity(value, axis) else {
|
||||
return Some(false);
|
||||
};
|
||||
if !target.is_finite() || capacity <= 1e-9 || target.abs() > capacity + 1e-12 {
|
||||
return Some(false);
|
||||
}
|
||||
let current_world = world_vector(
|
||||
value.base.transform,
|
||||
[value.direction.x, value.direction.y, value.direction.z],
|
||||
)?;
|
||||
let target = target.clamp(-capacity, capacity);
|
||||
if (current_world[axis] - target).abs() <= 1e-12 {
|
||||
return Some(false);
|
||||
}
|
||||
let component_axis = match axis {
|
||||
0 => glam::DVec3::X,
|
||||
1 => glam::DVec3::Y,
|
||||
_ => glam::DVec3::Z,
|
||||
};
|
||||
let Some(projected_axis) = (component_axis - normal * normal[axis]).try_normalize() else {
|
||||
return Some(false);
|
||||
};
|
||||
let other_axis = normal.cross(projected_axis);
|
||||
let along_projected = target / capacity;
|
||||
let remaining = (1.0 - along_projected * along_projected).max(0.0).sqrt();
|
||||
let other_sign = if current_world.dot(other_axis) < 0.0 {
|
||||
-1.0
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let world =
|
||||
projected_axis * along_projected + other_axis * (other_sign * remaining);
|
||||
let Some(local) = matrix(value.base.transform)?
|
||||
.inverse()
|
||||
.transform_vector3(world)
|
||||
.try_normalize()
|
||||
else {
|
||||
return Some(false);
|
||||
};
|
||||
let current = glam::DVec3::new(value.direction.x, value.direction.y, value.direction.z)
|
||||
.normalize_or_zero();
|
||||
if (local - current).length_squared() <= 1e-24 {
|
||||
return Some(false);
|
||||
}
|
||||
value.direction = acadrust::types::Vector3::new(local.x, local.y, local.z);
|
||||
return Some(true);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn apply_history_choice(
|
||||
document: &mut acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
@@ -1578,6 +1886,11 @@ pub fn apply_primitive_property(
|
||||
return applied;
|
||||
}
|
||||
}
|
||||
if let SolidHistoryOperation::Revolve(revolve_value) = operation {
|
||||
if let Some(applied) = apply_revolve_geometry_property(revolve_value, field, value) {
|
||||
return applied;
|
||||
}
|
||||
}
|
||||
if let SolidHistoryOperation::Torus(torus_value) = operation {
|
||||
if let Some(applied) = apply_torus_position_property(torus_value, field, value) {
|
||||
return applied;
|
||||
|
||||
@@ -18,6 +18,29 @@ fn tessellation(body: &Body) -> brep::mesh::BodyMesh {
|
||||
)
|
||||
}
|
||||
|
||||
fn display_tessellation(
|
||||
body: &Body,
|
||||
facet_resolution: f64,
|
||||
chordal_deflection: Option<f64>,
|
||||
isolines: usize,
|
||||
) -> brep::mesh::BodyMesh {
|
||||
let resolution = if facet_resolution.is_finite() && facet_resolution > 0.0 {
|
||||
facet_resolution.clamp(0.01, 10.0)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let max_angle = chordal_deflection.map_or_else(
|
||||
|| cadkernel::tessellation::angle_for_resolution(resolution),
|
||||
|_| cadkernel::tessellation::display_angle_for_resolution(resolution),
|
||||
);
|
||||
let mut tolerance = brep::mesh::TessellationTolerance::new(max_angle, TOL)
|
||||
.with_isolines(isolines);
|
||||
if let Some(deflection) = chordal_deflection {
|
||||
tolerance = tolerance.with_chordal_deflection(deflection);
|
||||
}
|
||||
brep::mesh::tessellate(body, tolerance)
|
||||
}
|
||||
|
||||
/// Axis-aligned box from its center and full extents.
|
||||
pub fn box_solid(center: [f64; 3], length: f64, width: f64, height: f64) -> Option<Body> {
|
||||
brep::make::cuboid(
|
||||
@@ -397,8 +420,13 @@ fn mesh_from_tessellation(
|
||||
color,
|
||||
selected: false,
|
||||
});
|
||||
for edge in tessellation.edges {
|
||||
for segment in edge.positions.windows(2) {
|
||||
for positions in tessellation
|
||||
.edges
|
||||
.into_iter()
|
||||
.map(|edge| edge.positions)
|
||||
.chain(tessellation.isolines.into_iter().map(|line| line.positions))
|
||||
{
|
||||
for segment in positions.windows(2) {
|
||||
for point in segment {
|
||||
let high = [point[0] as f32, point[1] as f32, point[2] as f32];
|
||||
set.edge_verts.push(high);
|
||||
@@ -418,9 +446,17 @@ fn mesh_from_tessellation(
|
||||
pub fn display_from_solid(
|
||||
body: &Body,
|
||||
color: [f32; 4],
|
||||
facet_resolution: f64,
|
||||
chordal_deflection: Option<f64>,
|
||||
isolines: usize,
|
||||
) -> Option<(MeshLodSet, Vec<acadrust::entities::Wire>, [f64; 3])> {
|
||||
use acadrust::types::Vector3;
|
||||
let tessellation = tessellation(body);
|
||||
let tessellation = display_tessellation(
|
||||
body,
|
||||
facet_resolution,
|
||||
chordal_deflection,
|
||||
isolines,
|
||||
);
|
||||
let center = mesh_center(&tessellation.mesh)?;
|
||||
let wires = tessellation
|
||||
.edges
|
||||
@@ -496,7 +532,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tri_count(body: &Body) -> usize {
|
||||
display_from_solid(body, [0.7, 0.7, 0.7, 1.0])
|
||||
display_from_solid(body, [0.7, 0.7, 0.7, 1.0], 1.0, None, 0)
|
||||
.map(|(m, _, _)| m.lods[0].indices.len() / 3)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
@@ -386,15 +386,65 @@ pub fn revolved(
|
||||
from: [f64; 3],
|
||||
to: [f64; 3],
|
||||
angle: f64,
|
||||
start_angle: f64,
|
||||
) -> Option<Body> {
|
||||
let profile = profile_of(entity)?;
|
||||
brep::revolve(
|
||||
profile.plane,
|
||||
&profile.pieces,
|
||||
from,
|
||||
[to[0] - from[0], to[1] - from[1], to[2] - from[2]],
|
||||
angle,
|
||||
)
|
||||
let axis = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
|
||||
let body = if let Some((plane, profiles)) =
|
||||
region_profiles(entity).filter(|(_, profiles)| profiles.len() > 1)
|
||||
{
|
||||
brep::revolve_region(plane, &profiles, from, axis, angle)?
|
||||
} else {
|
||||
let profile = profile_of(entity)?;
|
||||
brep::revolve(profile.plane, &profile.pieces, from, axis, angle)?
|
||||
};
|
||||
turn_revolved_body(body, from, axis, start_angle)
|
||||
}
|
||||
|
||||
/// REVOLVE sheet mode for an open or closed planar profile.
|
||||
pub fn revolved_surface(
|
||||
entity: &EntityType,
|
||||
from: [f64; 3],
|
||||
to: [f64; 3],
|
||||
angle: f64,
|
||||
start_angle: f64,
|
||||
) -> Option<Body> {
|
||||
let axis = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
|
||||
let body = if let Some((plane, profiles)) =
|
||||
region_profiles(entity).filter(|(_, profiles)| profiles.len() > 1)
|
||||
{
|
||||
brep::revolve_surface_region(plane, &profiles, from, axis, angle)?
|
||||
} else {
|
||||
let (profile, _) = extrusion_profile_of(entity)?;
|
||||
brep::revolve_surface(profile.plane, &profile.pieces, from, axis, angle)?
|
||||
};
|
||||
turn_revolved_body(body, from, axis, start_angle)
|
||||
}
|
||||
|
||||
fn turn_revolved_body(
|
||||
body: Body,
|
||||
pivot: [f64; 3],
|
||||
axis: [f64; 3],
|
||||
angle: f64,
|
||||
) -> Option<Body> {
|
||||
if !angle.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if angle.abs() <= 1e-12 {
|
||||
return Some(body);
|
||||
}
|
||||
let axis = Vec3::from(axis).normalize()?;
|
||||
let rotate = |vector: Vec3| {
|
||||
let (sin, cos) = angle.sin_cos();
|
||||
vector * cos + axis.cross(vector) * sin + axis * axis.dot(vector) * (1.0 - cos)
|
||||
};
|
||||
let pivot = Vec3::from(pivot);
|
||||
let placement = brep::Placement {
|
||||
x_axis: rotate(Vec3::X).to_array(),
|
||||
y_axis: rotate(Vec3::Y).to_array(),
|
||||
z_axis: rotate(Vec3::Z).to_array(),
|
||||
origin: (pivot - rotate(pivot)).to_array(),
|
||||
};
|
||||
brep::transform(&body, &placement)
|
||||
}
|
||||
|
||||
fn profile_center(profile: &Profile) -> Option<Vec3> {
|
||||
@@ -413,8 +463,13 @@ pub fn revolve_history(
|
||||
from: [f64; 3],
|
||||
to: [f64; 3],
|
||||
angle: f64,
|
||||
start_angle: f64,
|
||||
) -> Option<SolidHistoryOperation> {
|
||||
if !angle.is_finite() || angle.abs() <= 1e-12 {
|
||||
if !angle.is_finite()
|
||||
|| angle.abs() <= 1e-12
|
||||
|| !start_angle.is_finite()
|
||||
|| matches!(entity, EntityType::Region(region) if region.wires.len() > 1)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let profile = profile_of(entity)?;
|
||||
@@ -422,16 +477,27 @@ pub fn revolve_history(
|
||||
let direction = (Vec3::from(to) - from).normalize()?;
|
||||
let center = profile_center(&profile)?;
|
||||
let axis_point = from + direction * (center - from).dot(direction);
|
||||
let (sweep_entity, transform) = embedded_planar_entity(entity)?;
|
||||
let transform = glam::DMat4::from_cols_array(&transform);
|
||||
let inverse = transform.inverse();
|
||||
if !transform.is_finite() || !inverse.is_finite() || transform.determinant().abs() <= 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let axis_point = inverse.transform_point3(glam::DVec3::from_array(axis_point.to_array()));
|
||||
let direction = inverse
|
||||
.transform_vector3(glam::DVec3::from_array(direction.to_array()))
|
||||
.try_normalize()?;
|
||||
let mut base = SolidHistoryNodeBase::new(1);
|
||||
base.transform = glam::DMat4::IDENTITY.to_cols_array();
|
||||
base.transform = transform.to_cols_array();
|
||||
Some(SolidHistoryOperation::Revolve(SolidHistoryRevolve {
|
||||
base,
|
||||
operation_major: 1,
|
||||
axis_point: Vector3::new(axis_point.x, axis_point.y, axis_point.z),
|
||||
direction: Vector3::new(direction.x, direction.y, direction.z),
|
||||
revolve_angle: angle,
|
||||
start_angle,
|
||||
flag_290: true,
|
||||
sweep_entity: Some(embedded_path(entity)?),
|
||||
sweep_entity: Some(sweep_entity),
|
||||
..SolidHistoryRevolve::default()
|
||||
}))
|
||||
}
|
||||
@@ -597,6 +663,10 @@ fn embedded_planar_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64;
|
||||
Some((EmbeddedEntity::LwPolyline(polyline), transform))
|
||||
}
|
||||
|
||||
pub fn embedded_revolve_profile(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> {
|
||||
embedded_planar_entity(entity)
|
||||
}
|
||||
|
||||
pub fn extrusion_history(
|
||||
profile: &EntityType,
|
||||
path: Option<&EntityType>,
|
||||
@@ -850,6 +920,7 @@ mod tests {
|
||||
[0.0, 0.0, 0.0],
|
||||
[0.0, 10.0, 0.0],
|
||||
std::f64::consts::TAU,
|
||||
0.0,
|
||||
);
|
||||
// The profile is in the XY plane and the axis lies in it, so this is
|
||||
// a revolution about the y axis: radius ten, height ten.
|
||||
@@ -875,7 +946,8 @@ mod tests {
|
||||
&square(10.0),
|
||||
[0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 1.0],
|
||||
std::f64::consts::TAU
|
||||
std::f64::consts::TAU,
|
||||
0.0,
|
||||
)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
+13
-1
@@ -781,7 +781,19 @@ impl Scene {
|
||||
// ── Erase ─────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn erase_entities(&mut self, handles: &[Handle]) {
|
||||
self.remove_entities(handles, true);
|
||||
}
|
||||
|
||||
/// Roll back entities created by the current command after a downstream
|
||||
/// registration failure. These entities were never successful command
|
||||
/// results, so locked-layer edit policy must not prevent their removal.
|
||||
/// The shared removal path keeps first-touch undo state, history objects,
|
||||
/// groups, selection, and derived caches coherent.
|
||||
pub(crate) fn rollback_new_entities(&mut self, handles: &[Handle]) {
|
||||
self.remove_entities(handles, false);
|
||||
}
|
||||
|
||||
fn remove_entities(&mut self, handles: &[Handle], respect_layer_locks: bool) {
|
||||
let erase_handles = self.handles_expanded_for_leader_annotations(handles);
|
||||
|
||||
let mut handle_set: HashSet<Handle> = HashSet::default();
|
||||
@@ -791,7 +803,7 @@ impl Scene {
|
||||
|
||||
for &h in &erase_handles {
|
||||
// Objects on a locked layer can't be erased.
|
||||
if self.is_layer_locked(h) {
|
||||
if respect_layer_locks && self.is_layer_locked(h) {
|
||||
continue;
|
||||
}
|
||||
// Delta-undo: capture the removed entity so an undo can re-insert it.
|
||||
|
||||
Reference in new issue
Block a user