forked from 1b-agent/opencadstudio-upstream
Merge pull request #893 from ramox81/codex/complete-baseline-dimension
Complete DIMBASELINE workflow and creation policy
This commit is contained in:
20 files changed
+1198
-427
No files matched your search
@@ -856,7 +856,7 @@ impl OpenCADStudio {
|
||||
.infer_dimension_sources(handle);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.attach_dimension_association(handle, sources.to_vec());
|
||||
.attach_dimension_association(handle, sources);
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
@@ -1269,7 +1269,7 @@ impl OpenCADStudio {
|
||||
.infer_dimension_sources(handle);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.attach_dimension_association(handle, sources.to_vec());
|
||||
.attach_dimension_association(handle, sources);
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
@@ -1287,6 +1287,8 @@ impl OpenCADStudio {
|
||||
CmdResult::CommitDimension {
|
||||
mut entity,
|
||||
association,
|
||||
preserve_base_style,
|
||||
continue_command,
|
||||
} => {
|
||||
let label = self.history_label_from_active_cmd(i, "DIMENSION");
|
||||
let association_mode = self.tabs[i]
|
||||
@@ -1300,6 +1302,17 @@ impl OpenCADStudio {
|
||||
acadrust::entities::Dimension::Ordinate(_)
|
||||
)
|
||||
);
|
||||
let inherited_dimension = if preserve_base_style {
|
||||
match &entity {
|
||||
acadrust::EntityType::Dimension(dimension) => Some((
|
||||
dimension.base().common.layer.clone(),
|
||||
dimension.base().style_name.clone(),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let pending = if association_mode == 0 {
|
||||
let layer = self.tabs[i].active_layer.clone();
|
||||
if layer != "0" || entity.as_entity().layer().is_empty() {
|
||||
@@ -1330,6 +1343,12 @@ impl OpenCADStudio {
|
||||
&self.tabs[i].scene.document,
|
||||
&mut entity,
|
||||
);
|
||||
if let (Some((layer, style_name)), acadrust::EntityType::Dimension(dimension)) =
|
||||
(inherited_dimension, &mut entity)
|
||||
{
|
||||
dimension.base_mut().common.layer = layer;
|
||||
dimension.base_mut().style_name = style_name;
|
||||
}
|
||||
let pieces = crate::modules::draw::modify::explode::explode_entity(
|
||||
&entity,
|
||||
&self.tabs[i].scene.document,
|
||||
@@ -1345,7 +1364,10 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
let delta_safe = self.delta_add_safe(i, &entity);
|
||||
let pending = self.begin_undo(i, label, 1, delta_safe);
|
||||
if let Some(handle) = self.commit_entity_handle(entity) {
|
||||
if let Some(handle) = self.commit_entity_handle_with_dimension_policy(
|
||||
entity,
|
||||
preserve_base_style,
|
||||
) {
|
||||
if association_mode == 2 {
|
||||
let mut changes = vec![
|
||||
(handle, crate::scene::ChangeKind::Modified),
|
||||
@@ -1357,8 +1379,6 @@ impl OpenCADStudio {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.infer_dimension_sources(handle)
|
||||
.into_iter()
|
||||
.collect()
|
||||
},
|
||||
|source| {
|
||||
if single_source_dimension {
|
||||
@@ -1393,12 +1413,22 @@ impl OpenCADStudio {
|
||||
};
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
if continue_command {
|
||||
if let Some(prompt) = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|cmd| cmd.prompt())
|
||||
{
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
} else {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
}
|
||||
}
|
||||
CmdResult::CommitSolid {
|
||||
entity,
|
||||
|
||||
+36
-88
@@ -646,36 +646,50 @@ impl OpenCADStudio {
|
||||
|
||||
"DIMCONTINUE" => {
|
||||
use crate::modules::annotate::dim_continue::DimContinueCommand;
|
||||
let cmd = if let Some((p1, p2, dp, rot, trot)) =
|
||||
find_last_linear_dim(&self.tabs[i].scene)
|
||||
{
|
||||
DimContinueCommand::from_base(p1, p2, dp, rot, trot)
|
||||
} else {
|
||||
DimContinueCommand::new()
|
||||
};
|
||||
let scene = &self.tabs[i].scene;
|
||||
let preserve_base_style = self.dimension_continue_mode == 1;
|
||||
let cmd = scene
|
||||
.last_created_dimension
|
||||
.filter(|handle| scene.entity_belongs_to_active_space(*handle))
|
||||
.and_then(|handle| scene.document.get_entity(handle))
|
||||
.and_then(|entity| match entity {
|
||||
acadrust::EntityType::Dimension(dimension) => Some(
|
||||
DimContinueCommand::from_dimension(dimension, preserve_base_style),
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| DimContinueCommand::new(preserve_base_style));
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
"DIMBASELINE" => {
|
||||
use crate::modules::annotate::dim_baseline::DimBaselineCommand;
|
||||
let cmd = if let Some((p1, p2, dp, rot, trot)) =
|
||||
find_last_linear_dim(&self.tabs[i].scene)
|
||||
{
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let dimdli = doc
|
||||
.dim_styles
|
||||
.iter()
|
||||
.find(|s| {
|
||||
s.name
|
||||
.eq_ignore_ascii_case(&doc.header.current_dimstyle_name)
|
||||
})
|
||||
.map(|s| s.dimdli as f32)
|
||||
.unwrap_or(1.5);
|
||||
DimBaselineCommand::from_base(p1, p2, dp, rot, trot, dimdli)
|
||||
let scene = &self.tabs[i].scene;
|
||||
let recent = scene
|
||||
.last_created_dimension
|
||||
.filter(|handle| scene.entity_belongs_to_active_space(*handle))
|
||||
.and_then(|handle| scene.document.get_entity(handle))
|
||||
.cloned();
|
||||
let dimdli_by_style = scene
|
||||
.document
|
||||
.dim_styles
|
||||
.iter()
|
||||
.map(|style| (style.name.to_ascii_lowercase(), style.dimdli))
|
||||
.collect();
|
||||
let fallback_dimdli = if scene.document.header.measurement == 1 {
|
||||
3.75
|
||||
} else {
|
||||
DimBaselineCommand::new()
|
||||
0.38
|
||||
};
|
||||
let current_style_name = scene.document.header.current_dimstyle_name.clone();
|
||||
let cmd = DimBaselineCommand::new(
|
||||
recent,
|
||||
dimdli_by_style,
|
||||
current_style_name,
|
||||
fallback_dimdli,
|
||||
self.dimension_continue_mode == 1,
|
||||
);
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
@@ -1376,72 +1390,6 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the last placed linear or aligned dimension in the document.
|
||||
/// Returns `(first_point, second_point, definition_point, rotation_rad)` in world-space.
|
||||
fn find_last_linear_dim(
|
||||
scene: &crate::scene::Scene,
|
||||
) -> Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64, f64)> {
|
||||
use acadrust::entities::Dimension;
|
||||
let mut best_handle: u64 = 0;
|
||||
let mut result: Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64, f64)> = None;
|
||||
|
||||
for entity in scene.document.entities() {
|
||||
if let acadrust::EntityType::Dimension(dim) = entity {
|
||||
let h = entity.common().handle.value();
|
||||
if h <= best_handle {
|
||||
continue;
|
||||
}
|
||||
let item = match dim {
|
||||
Dimension::Linear(d) => {
|
||||
let p1 = glam::Vec3::new(
|
||||
d.first_point.x as f32,
|
||||
d.first_point.y as f32,
|
||||
d.first_point.z as f32,
|
||||
);
|
||||
let p2 = glam::Vec3::new(
|
||||
d.second_point.x as f32,
|
||||
d.second_point.y as f32,
|
||||
d.second_point.z as f32,
|
||||
);
|
||||
let dp = glam::Vec3::new(
|
||||
d.base.definition_point.x as f32,
|
||||
d.base.definition_point.y as f32,
|
||||
d.base.definition_point.z as f32,
|
||||
);
|
||||
Some((p1, p2, dp, d.rotation, d.base.text_rotation))
|
||||
}
|
||||
Dimension::Aligned(d) => {
|
||||
let p1 = glam::Vec3::new(
|
||||
d.first_point.x as f32,
|
||||
d.first_point.y as f32,
|
||||
d.first_point.z as f32,
|
||||
);
|
||||
let p2 = glam::Vec3::new(
|
||||
d.second_point.x as f32,
|
||||
d.second_point.y as f32,
|
||||
d.second_point.z as f32,
|
||||
);
|
||||
let dp = glam::Vec3::new(
|
||||
d.base.definition_point.x as f32,
|
||||
d.base.definition_point.y as f32,
|
||||
d.base.definition_point.z as f32,
|
||||
);
|
||||
let dx = (d.second_point.x - d.first_point.x) as f32;
|
||||
let dy = (d.second_point.y - d.first_point.y) as f32;
|
||||
let rot = dy.atan2(dx) as f64;
|
||||
Some((p1, p2, dp, rot, d.base.text_rotation))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(data) = item {
|
||||
best_handle = h;
|
||||
result = Some(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Collect candidate dimension endpoints from an entity for QDIM — line ends,
|
||||
/// polyline vertices, arc endpoints — in world space.
|
||||
fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec<glam::DVec3>) {
|
||||
|
||||
@@ -931,6 +931,7 @@ impl OpenCADStudio {
|
||||
| "CENTERCROSSSIZE"
|
||||
| "CENTERCROSSGAP"
|
||||
| "CENTERMARKEXE"
|
||||
| "DIMCONTINUEMODE"
|
||||
) =>
|
||||
{
|
||||
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
||||
@@ -953,7 +954,7 @@ impl OpenCADStudio {
|
||||
let value = it.next().map(|s| s.trim().to_string());
|
||||
if name.is_empty() || name == "?" {
|
||||
self.command_line.push_info(
|
||||
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
||||
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
||||
);
|
||||
} else {
|
||||
let frame_kind = crate::scene::frame::kind_for_name(&name);
|
||||
@@ -1069,6 +1070,28 @@ impl OpenCADStudio {
|
||||
}
|
||||
return Some(self.finish_dispatch(cmd));
|
||||
}
|
||||
if name == "DIMCONTINUEMODE" {
|
||||
let current = self.dimension_continue_mode;
|
||||
if let Some(value) = &value {
|
||||
match value.parse::<i16>() {
|
||||
Ok(mode @ 0..=1) => {
|
||||
self.dimension_continue_mode = mode;
|
||||
self.persist_settings_if_changed();
|
||||
self.command_line
|
||||
.push_output(&format!("DIMCONTINUEMODE = {mode}"));
|
||||
}
|
||||
_ => self.command_line.push_error(
|
||||
"SETVAR: DIMCONTINUEMODE requires 0 or 1.",
|
||||
),
|
||||
}
|
||||
} else {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"Enter new value for {name} <{current}>:"
|
||||
).as_ref());
|
||||
self.pending_setvar = Some(name.clone());
|
||||
}
|
||||
return Some(self.finish_dispatch(cmd));
|
||||
}
|
||||
if matches!(name.as_str(), "DONUTID" | "DONUTOD") {
|
||||
let current = if name == "DONUTID" {
|
||||
crate::modules::draw::defaults::get_donut_inner_diameter()
|
||||
|
||||
@@ -465,6 +465,8 @@ pub(super) struct OpenCADStudio {
|
||||
options_tab: crate::ui::window::options::OptionsTab,
|
||||
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
||||
pub texteditmode: bool,
|
||||
/// Creation-style policy used by continuing and baseline dimensions.
|
||||
pub dimension_continue_mode: i16,
|
||||
/// When true (default), saving over an existing file first writes a `.bak`
|
||||
/// copy of it for recovery (#205). Toggle with the ISAVEBAK command.
|
||||
pub backup_on_save: bool,
|
||||
@@ -3210,6 +3212,7 @@ impl OpenCADStudio {
|
||||
dyn_input: true,
|
||||
options_tab: crate::ui::window::options::OptionsTab::General,
|
||||
texteditmode: false,
|
||||
dimension_continue_mode: 1,
|
||||
backup_on_save: true,
|
||||
file_assoc_enabled: true,
|
||||
savetime_min: 10,
|
||||
|
||||
@@ -2473,10 +2473,33 @@ impl OpenCADStudio {
|
||||
/// (or `None` if it could not be added). Lets callers follow up — e.g.
|
||||
/// open the in-place text editor on a freshly created MultiLeader.
|
||||
pub(super) fn commit_entity_handle(
|
||||
&mut self,
|
||||
entity: acadrust::EntityType,
|
||||
) -> Option<Handle> {
|
||||
self.commit_entity_handle_with_dimension_policy(entity, false)
|
||||
}
|
||||
|
||||
/// Commit an entity while optionally retaining a dimension's source layer
|
||||
/// and dimension style. DIMCONTINUEMODE=1 uses this path; ordinary
|
||||
/// creation continues to receive the current ribbon/style defaults.
|
||||
pub(super) fn commit_entity_handle_with_dimension_policy(
|
||||
&mut self,
|
||||
mut entity: acadrust::EntityType,
|
||||
preserve_dimension_layer_and_style: bool,
|
||||
) -> Option<Handle> {
|
||||
let i = self.active_tab;
|
||||
let is_dimension = matches!(&entity, acadrust::EntityType::Dimension(_));
|
||||
let inherited_dimension = if preserve_dimension_layer_and_style {
|
||||
match &entity {
|
||||
acadrust::EntityType::Dimension(dimension) => Some((
|
||||
dimension.base().common.layer.clone(),
|
||||
dimension.base().style_name.clone(),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let acadrust::EntityType::Table(table) = &mut entity {
|
||||
const PREFIX: &str = "__OPENCAD_LINK_PENDING__";
|
||||
if let Some(path) = table.name.strip_prefix(PREFIX).map(str::to_string) {
|
||||
@@ -2578,6 +2601,12 @@ impl OpenCADStudio {
|
||||
&self.tabs[i].scene.document,
|
||||
&mut entity,
|
||||
);
|
||||
if let (Some((layer, style_name)), acadrust::EntityType::Dimension(dimension)) =
|
||||
(inherited_dimension, &mut entity)
|
||||
{
|
||||
dimension.base_mut().common.layer = layer;
|
||||
dimension.base_mut().style_name = style_name;
|
||||
}
|
||||
|
||||
// Smart centre objects carry their own drawing-level creation style.
|
||||
// Apply it after the generic ribbon style so ordinary LINE entities
|
||||
@@ -2737,6 +2766,9 @@ impl OpenCADStudio {
|
||||
self.tabs[i].last_draw_anchor = Some(handle);
|
||||
}
|
||||
}
|
||||
if is_dimension {
|
||||
self.tabs[i].scene.last_created_dimension = new_handle;
|
||||
}
|
||||
new_handle
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +198,10 @@ pub struct UserSettings {
|
||||
pub osmode: i32,
|
||||
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
||||
pub texteditmode: bool,
|
||||
/// DIMCONTINUEMODE: 1 inherits the base dimension's layer/style; 0 uses
|
||||
/// the current creation layer/style. Registry-style, app-level preference.
|
||||
#[serde(default = "default_dimension_continue_mode")]
|
||||
pub dimension_continue_mode: i16,
|
||||
/// TEXTFILL: fill TrueType glyphs (true) or draw them hollow (false).
|
||||
pub textfill: bool,
|
||||
/// When true, saving over an existing file first copies it to a sibling
|
||||
@@ -247,6 +251,10 @@ fn default_clipromptlines() -> i32 {
|
||||
3
|
||||
}
|
||||
|
||||
fn default_dimension_continue_mode() -> i16 {
|
||||
1
|
||||
}
|
||||
|
||||
fn deserialize_clipromptlines<'de, D>(de: D) -> Result<i32, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
@@ -284,6 +292,7 @@ impl Default for UserSettings {
|
||||
// off (suppress bit 16384).
|
||||
osmode: 575 | OSMODE_SUPPRESS,
|
||||
texteditmode: false,
|
||||
dimension_continue_mode: 1,
|
||||
textfill: true,
|
||||
backup_on_save: true,
|
||||
file_assoc_enabled: true,
|
||||
|
||||
@@ -382,6 +382,7 @@ impl OpenCADStudio {
|
||||
self.snapper.snap_enabled,
|
||||
),
|
||||
texteditmode: self.texteditmode,
|
||||
dimension_continue_mode: self.dimension_continue_mode,
|
||||
textfill: crate::scene::text::sdf_atlas::textfill(),
|
||||
backup_on_save: self.backup_on_save,
|
||||
file_assoc_enabled: self.file_assoc_enabled,
|
||||
@@ -440,6 +441,7 @@ impl OpenCADStudio {
|
||||
self.snapper.enabled = modes.into_iter().collect();
|
||||
self.snapper.snap_enabled = snap_enabled;
|
||||
self.texteditmode = s.texteditmode;
|
||||
self.dimension_continue_mode = s.dimension_continue_mode.clamp(0, 1);
|
||||
crate::scene::text::sdf_atlas::set_textfill(s.textfill);
|
||||
self.backup_on_save = s.backup_on_save;
|
||||
self.file_assoc_enabled = s.file_assoc_enabled;
|
||||
|
||||
@@ -1248,6 +1248,11 @@ pub enum CmdResult {
|
||||
CommitDimension {
|
||||
entity: EntityType,
|
||||
association: DimensionAssociationInput,
|
||||
/// Retain the source dimension's layer and style instead of replacing
|
||||
/// them with the current creation defaults (DIMCONTINUEMODE=1).
|
||||
preserve_base_style: bool,
|
||||
/// Keep collecting points after the dimension is committed.
|
||||
continue_command: bool,
|
||||
},
|
||||
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
|
||||
/// persistence) plus its B-rep (cached for boolean ops + shaded
|
||||
|
||||
@@ -135,6 +135,8 @@ impl CadCommand for AlignedDimensionCommand {
|
||||
CmdResult::CommitDimension {
|
||||
entity: self.plane.place_entity(EntityType::Dimension(Dimension::Aligned(dim))),
|
||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +234,8 @@ impl AngularDimensionCommand {
|
||||
CmdResult::CommitDimension {
|
||||
entity,
|
||||
association: DimensionAssociationInput::Explicit(self.source_refs.clone()),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -265,6 +265,8 @@ impl ArcLengthDimensionCommand {
|
||||
CmdResult::CommitDimension {
|
||||
entity: plane.place_entity(EntityType::Dimension(Dimension::Arc(dimension))),
|
||||
association,
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,8 @@ impl CadCommand for DiameterDimensionCommand {
|
||||
entity: source_plane
|
||||
.place_entity(EntityType::Dimension(Dimension::Diameter(dim))),
|
||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,17 +1,10 @@
|
||||
// DIMCONTINUE command — chain linear/aligned dimensions end-to-end.
|
||||
//
|
||||
// Each new point becomes the second extension line origin of a new dimension,
|
||||
// whose first extension line origin is the second extension line of the previous dim.
|
||||
// The dimension line stays at the same perpendicular offset as the base dimension.
|
||||
//
|
||||
// Constructed from commands.rs after finding the last placed linear/aligned dimension.
|
||||
|
||||
use acadrust::entities::{Dimension, DimensionLinear};
|
||||
use acadrust::entities::{Dimension, DimensionBase, DimensionLinear};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use glam::{DVec3, Vec3};
|
||||
use cadkernel::space::Plane;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, DimensionAssociationInput};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
@@ -27,75 +20,112 @@ pub fn tool() -> ToolDef {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DimContinueCommand {
|
||||
/// Fixed first-extension-line origin for the current step (moves each iteration).
|
||||
chain_p1: DVec3,
|
||||
/// Direction along the dimension axis (0.0 = horizontal, PI/2 = vertical).
|
||||
rotation: f64,
|
||||
/// Text reading rotation inherited from the base dim so a UCS-aligned chain
|
||||
/// keeps its text consistent with the originating DIMLINEAR. 0 = natural.
|
||||
#[derive(Clone)]
|
||||
struct SourceStyle {
|
||||
layer: String,
|
||||
style_name: String,
|
||||
normal: Vector3,
|
||||
text_rotation: f64,
|
||||
/// Absolute perpendicular coordinate (dot with `perp`) of the base
|
||||
/// dimension line. Each continued dim line is projected onto this exact
|
||||
/// coordinate so the whole chain stays collinear — even when the extension
|
||||
/// origins sit at different perpendicular positions (extension lines of
|
||||
/// different lengths). (#151)
|
||||
dim_line_perp: f64,
|
||||
/// Direction of "up" perpendicular to the dim axis (points toward the dim line).
|
||||
perp: DVec3,
|
||||
/// True once we have a base dimension loaded.
|
||||
ready: bool,
|
||||
horizontal_direction: f64,
|
||||
}
|
||||
|
||||
impl DimContinueCommand {
|
||||
/// No base dim found — will show an error prompt and cancel immediately.
|
||||
pub fn new() -> Self {
|
||||
impl SourceStyle {
|
||||
fn from_base(base: &DimensionBase) -> Self {
|
||||
Self {
|
||||
chain_p1: DVec3::ZERO,
|
||||
rotation: 0.0,
|
||||
text_rotation: 0.0,
|
||||
dim_line_perp: 0.0,
|
||||
perp: DVec3::Y,
|
||||
ready: false,
|
||||
layer: base.common.layer.clone(),
|
||||
style_name: base.style_name.clone(),
|
||||
normal: base.normal,
|
||||
text_rotation: base.text_rotation,
|
||||
horizontal_direction: base.horizontal_direction,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build from the last placed dimension.
|
||||
///
|
||||
/// `p2` — second extension line origin of the base dim (the chain starts
|
||||
/// here). `p1` is unused — the dim line position comes from
|
||||
/// `definition_point` alone — but kept for signature parity with the
|
||||
/// shared `find_last_linear_dim` tuple (DIMBASELINE uses p1).
|
||||
/// `definition_point` — where the dim line was placed; its perpendicular
|
||||
/// coordinate is the line the whole chain stays collinear with.
|
||||
/// `rotation` — 0.0 = horizontal dim, PI/2 = vertical dim.
|
||||
pub fn from_base(
|
||||
_p1: Vec3,
|
||||
p2: Vec3,
|
||||
definition_point: Vec3,
|
||||
rotation: f64,
|
||||
text_rotation: f64,
|
||||
) -> Self {
|
||||
// Widen the base-dim points to f64 so the committed chain math runs in
|
||||
// full precision. (The base points arrive as f32 from the entity, but
|
||||
// every derived committed coordinate must stay f64 from here on.)
|
||||
let p2 = p2.as_dvec3();
|
||||
let definition_point = definition_point.as_dvec3();
|
||||
// Axis unit vector along the measurement direction — the base dim's
|
||||
// rotation angle (any angle, incl. a UCS-aligned one), not a world H/V.
|
||||
let axis = DVec3::new(rotation.cos(), rotation.sin(), 0.0);
|
||||
// Perpendicular unit vector toward the dim line.
|
||||
let perp = DVec3::new(-axis.y, axis.x, 0.0);
|
||||
let dim_line_perp = definition_point.dot(perp);
|
||||
fn apply(&self, base: &mut DimensionBase) {
|
||||
base.common.layer = self.layer.clone();
|
||||
base.style_name = self.style_name.clone();
|
||||
base.normal = self.normal;
|
||||
base.text_rotation = self.text_rotation;
|
||||
base.horizontal_direction = self.horizontal_direction;
|
||||
}
|
||||
}
|
||||
|
||||
struct ContinueState {
|
||||
chain_p1: [f64; 2],
|
||||
rotation: f64,
|
||||
dim_line_perp: f64,
|
||||
perpendicular: [f64; 2],
|
||||
plane: Plane,
|
||||
style: SourceStyle,
|
||||
}
|
||||
|
||||
pub struct DimContinueCommand {
|
||||
state: Option<ContinueState>,
|
||||
preserve_base_style: bool,
|
||||
}
|
||||
|
||||
impl DimContinueCommand {
|
||||
pub fn new(preserve_base_style: bool) -> Self {
|
||||
Self {
|
||||
chain_p1: p2,
|
||||
rotation,
|
||||
text_rotation,
|
||||
dim_line_perp,
|
||||
perp,
|
||||
ready: true,
|
||||
state: None,
|
||||
preserve_base_style,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_dimension(dimension: &Dimension, preserve_base_style: bool) -> Self {
|
||||
let (first, second, definition, rotation) = match dimension {
|
||||
Dimension::Linear(source) => (
|
||||
source.first_point,
|
||||
source.second_point,
|
||||
source.base.definition_point,
|
||||
source.rotation,
|
||||
),
|
||||
Dimension::Aligned(source) => {
|
||||
let plane = plane_from_normal(source.first_point, source.base.normal);
|
||||
let first = plane.project(point(source.first_point)).unwrap_or([0.0; 2]);
|
||||
let second = plane.project(point(source.second_point)).unwrap_or(first);
|
||||
let delta = [second[0] - first[0], second[1] - first[1]];
|
||||
(
|
||||
source.first_point,
|
||||
source.second_point,
|
||||
source.base.definition_point,
|
||||
delta[1].atan2(delta[0]),
|
||||
)
|
||||
}
|
||||
_ => return Self::new(preserve_base_style),
|
||||
};
|
||||
let plane = plane_from_normal(first, dimension.base().normal);
|
||||
let second = plane.project(point(second)).unwrap_or([0.0; 2]);
|
||||
let definition = plane.project(point(definition)).unwrap_or(second);
|
||||
let perpendicular = [-rotation.sin(), rotation.cos()];
|
||||
let dim_line_perp = dot(definition, perpendicular);
|
||||
Self {
|
||||
state: Some(ContinueState {
|
||||
chain_p1: second,
|
||||
rotation,
|
||||
dim_line_perp,
|
||||
perpendicular,
|
||||
plane,
|
||||
style: SourceStyle::from_base(dimension.base()),
|
||||
}),
|
||||
preserve_base_style,
|
||||
}
|
||||
}
|
||||
|
||||
fn placement(&self, point_world: DVec3) -> Option<([f64; 2], [f64; 2], [f64; 2])> {
|
||||
let state = self.state.as_ref()?;
|
||||
let second = state.plane.project(point_world.to_array())?;
|
||||
let first_line = project_to_line(
|
||||
state.chain_p1,
|
||||
state.perpendicular,
|
||||
state.dim_line_perp,
|
||||
);
|
||||
let second_line = project_to_line(
|
||||
second,
|
||||
state.perpendicular,
|
||||
state.dim_line_perp,
|
||||
);
|
||||
Some((second, first_line, second_line))
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DimContinueCommand {
|
||||
@@ -104,61 +134,61 @@ impl CadCommand for DimContinueCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if !self.ready {
|
||||
if self.state.is_none() {
|
||||
t!("DIMCONTINUE No base dimension found. Place a dimension first.").into_owned()
|
||||
} else {
|
||||
t!("DIMCONTINUE Specify a second extension line origin (Enter to exit):").into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if !self.ready {
|
||||
fn on_point(&mut self, point_world: DVec3) -> CmdResult {
|
||||
let Some((second, first_line, second_line)) = self.placement(point_world) else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
let state = self.state.as_mut().expect("continue state");
|
||||
let mut dimension = DimensionLinear::new(
|
||||
vector3(state.plane.point_at(state.chain_p1)),
|
||||
vector3(state.plane.point_at(second)),
|
||||
);
|
||||
dimension.rotation = state.rotation;
|
||||
state.style.apply(&mut dimension.base);
|
||||
dimension.definition_point = vector3(state.plane.point_at(first_line));
|
||||
dimension.base.definition_point = dimension.definition_point;
|
||||
dimension.base.text_middle_point = vector3(state.plane.point_at([
|
||||
(first_line[0] + second_line[0]) * 0.5,
|
||||
(first_line[1] + second_line[1]) * 0.5,
|
||||
]));
|
||||
dimension.base.insertion_point = dimension.base.text_middle_point;
|
||||
let axis = [state.rotation.cos(), state.rotation.sin()];
|
||||
dimension.base.actual_measurement = dot(
|
||||
[
|
||||
second[0] - state.chain_p1[0],
|
||||
second[1] - state.chain_p1[1],
|
||||
],
|
||||
axis,
|
||||
)
|
||||
.abs();
|
||||
state.chain_p1 = second;
|
||||
|
||||
CmdResult::CommitDimension {
|
||||
entity: EntityType::Dimension(Dimension::Linear(dimension)),
|
||||
association: DimensionAssociationInput::Infer(None),
|
||||
preserve_base_style: self.preserve_base_style,
|
||||
continue_command: true,
|
||||
}
|
||||
let p1 = self.chain_p1;
|
||||
let p2 = pt;
|
||||
|
||||
// Build a new linear dimension.
|
||||
let mut dim = DimensionLinear::new(v3(p1), v3(p2));
|
||||
dim.rotation = self.rotation;
|
||||
if self.text_rotation.abs() > 1e-9 {
|
||||
dim.base.text_rotation = self.text_rotation;
|
||||
}
|
||||
|
||||
// Dim line stays collinear with the base: project both extension
|
||||
// origins onto the base dim line's absolute perpendicular coordinate.
|
||||
// (A fixed offset from p1 drifts off the base line whenever p1 and p2
|
||||
// sit at different perpendicular positions.) (#151)
|
||||
let d1 = p1 + self.perp * (self.dim_line_perp - p1.dot(self.perp));
|
||||
let d2 = p2 + self.perp * (self.dim_line_perp - p2.dot(self.perp));
|
||||
dim.definition_point = v3(d1);
|
||||
dim.base.definition_point = v3(d1);
|
||||
dim.base.text_middle_point = v3((d1 + d2) * 0.5);
|
||||
dim.base.insertion_point = dim.base.text_middle_point;
|
||||
dim.base.actual_measurement = dim.measurement();
|
||||
|
||||
// Advance chain: next dim's P1 = this dim's P2.
|
||||
self.chain_p1 = p2;
|
||||
|
||||
CmdResult::CommitEntity(EntityType::Dimension(Dimension::Linear(dim)))
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
if !self.ready {
|
||||
return None;
|
||||
}
|
||||
// Preview / rubber-band geometry feeds an f32 GPU buffer, so drop to f32
|
||||
// at this screen-only boundary.
|
||||
let pt = pt.as_vec3();
|
||||
let p1 = self.chain_p1.as_vec3();
|
||||
let perp = self.perp.as_vec3();
|
||||
let dim_line_perp = self.dim_line_perp as f32;
|
||||
let dim_line_pt = p1 + perp * (dim_line_perp - p1.dot(perp));
|
||||
let dim_line_pt2 = pt + perp * (dim_line_perp - pt.dot(perp));
|
||||
fn on_mouse_move(&mut self, point_world: DVec3) -> Option<WireModel> {
|
||||
let (second, first_line, second_line) = self.placement(point_world)?;
|
||||
let state = self.state.as_ref()?;
|
||||
let first = state.plane.point_at(state.chain_p1);
|
||||
let second = state.plane.point_at(second);
|
||||
let first_line = state.plane.point_at(first_line);
|
||||
let second_line = state.plane.point_at(second_line);
|
||||
Some(WireModel {
|
||||
point_marker: None,
|
||||
taper_widths: Vec::new(),
|
||||
@@ -177,14 +207,14 @@ impl CadCommand for DimContinueCommand {
|
||||
text_verts: Vec::new(),
|
||||
name: "dimcont_preview".into(),
|
||||
points: vec![
|
||||
[p1.x, p1.y, p1.z],
|
||||
[dim_line_pt.x, dim_line_pt.y, dim_line_pt.z],
|
||||
float3(first),
|
||||
float3(first_line),
|
||||
[f32::NAN, 0.0, 0.0],
|
||||
[pt.x, pt.y, pt.z],
|
||||
[dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z],
|
||||
float3(second),
|
||||
float3(second_line),
|
||||
[f32::NAN, 0.0, 0.0],
|
||||
[dim_line_pt.x, dim_line_pt.y, dim_line_pt.z],
|
||||
[dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z],
|
||||
float3(first_line),
|
||||
float3(second_line),
|
||||
],
|
||||
points_low: Vec::new(),
|
||||
color: WireModel::CYAN,
|
||||
@@ -192,22 +222,50 @@ impl CadCommand for DimContinueCommand {
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
snap_pts: Vec::new(),
|
||||
tangent_geoms: Vec::new(),
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
key_vertices: Vec::new(),
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
fill_tris: vec![],
|
||||
fill_tris: Vec::new(),
|
||||
fill_tris_low: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn v3(p: DVec3) -> Vector3 {
|
||||
Vector3::new(p.x, p.y, p.z)
|
||||
fn plane_from_normal(origin: Vector3, normal: Vector3) -> Plane {
|
||||
let (x_axis, y_axis) =
|
||||
crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z));
|
||||
Plane::from_axes(
|
||||
point(origin),
|
||||
[x_axis.0, x_axis.1, x_axis.2],
|
||||
[y_axis.0, y_axis.1, y_axis.2],
|
||||
)
|
||||
}
|
||||
|
||||
fn project_to_line(point: [f64; 2], perpendicular: [f64; 2], offset: f64) -> [f64; 2] {
|
||||
let distance = offset - dot(point, perpendicular);
|
||||
[
|
||||
point[0] + perpendicular[0] * distance,
|
||||
point[1] + perpendicular[1] * distance,
|
||||
]
|
||||
}
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] }); // DimContinueCommand
|
||||
fn dot(first: [f64; 2], second: [f64; 2]) -> f64 {
|
||||
first[0] * second[0] + first[1] * second[1]
|
||||
}
|
||||
|
||||
fn point(value: Vector3) -> [f64; 3] {
|
||||
[value.x, value.y, value.z]
|
||||
}
|
||||
|
||||
fn vector3(value: [f64; 3]) -> Vector3 {
|
||||
Vector3::new(value[0], value[1], value[2])
|
||||
}
|
||||
|
||||
fn float3(value: [f64; 3]) -> [f32; 3] {
|
||||
[value[0] as f32, value[1] as f32, value[2] as f32]
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] });
|
||||
@@ -139,6 +139,8 @@ impl JoggedRadiusDimensionCommand {
|
||||
dimension,
|
||||
))),
|
||||
association,
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,8 @@ impl CadCommand for LinearDimensionCommand {
|
||||
CmdResult::CommitDimension {
|
||||
entity,
|
||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@ impl CadCommand for OrdinateDimCommand {
|
||||
Dimension::Ordinate(dim),
|
||||
)),
|
||||
association: DimensionAssociationInput::Infer(None),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,8 @@ impl CadCommand for RadiusDimensionCommand {
|
||||
CmdResult::CommitDimension {
|
||||
entity: source_plane.place_entity(EntityType::Dimension(Dimension::Radius(dim))),
|
||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,12 +408,23 @@ fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Opti
|
||||
.copied()
|
||||
}
|
||||
|
||||
fn dimension_inference_points(dimension: &Dimension) -> Option<[Option<Vector3>; 2]> {
|
||||
fn dimension_inference_points(dimension: &Dimension) -> Vec<Option<Vector3>> {
|
||||
match dimension {
|
||||
Dimension::Linear(linear) => Some([Some(linear.first_point), Some(linear.second_point)]),
|
||||
Dimension::Aligned(aligned) => Some([Some(aligned.first_point), Some(aligned.second_point)]),
|
||||
Dimension::Ordinate(ordinate) => Some([Some(ordinate.feature_location), None]),
|
||||
_ => None,
|
||||
Dimension::Linear(linear) => vec![Some(linear.first_point), Some(linear.second_point)],
|
||||
Dimension::Aligned(aligned) => vec![Some(aligned.first_point), Some(aligned.second_point)],
|
||||
Dimension::Angular3Pt(angular) => vec![
|
||||
Some(angular.angle_vertex),
|
||||
Some(angular.first_point),
|
||||
Some(angular.second_point),
|
||||
],
|
||||
Dimension::Angular2Ln(angular) => vec![
|
||||
Some(angular.first_point),
|
||||
Some(angular.second_point),
|
||||
Some(angular.angle_vertex),
|
||||
Some(angular.definition_point),
|
||||
],
|
||||
Dimension::Ordinate(ordinate) => vec![Some(ordinate.feature_location)],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,10 +811,10 @@ impl Scene {
|
||||
pub(crate) fn infer_dimension_sources(
|
||||
&self,
|
||||
dimension: Handle,
|
||||
) -> [Option<Handle>; 2] {
|
||||
) -> Vec<Option<Handle>> {
|
||||
let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension)
|
||||
else {
|
||||
return [None, None];
|
||||
return Vec::new();
|
||||
};
|
||||
let radial_data = match entity {
|
||||
Dimension::Radius(radius) => Some((
|
||||
@@ -845,23 +856,25 @@ impl Scene {
|
||||
})
|
||||
.min_by(|first, second| first.0.total_cmp(&second.0))
|
||||
.map(|(_, handle)| handle);
|
||||
return [source, None];
|
||||
return vec![source];
|
||||
}
|
||||
let Some(points) = dimension_inference_points(entity) else {
|
||||
return [None, None];
|
||||
};
|
||||
points.map(|point| {
|
||||
point.and_then(|point| self.document
|
||||
.entities()
|
||||
.filter(|entity| entity.common().handle != dimension)
|
||||
.filter_map(|entity| {
|
||||
source_distance_squared(entity, point)
|
||||
.map(|distance| (distance, entity.common().handle))
|
||||
dimension_inference_points(entity)
|
||||
.into_iter()
|
||||
.map(|point| {
|
||||
point.and_then(|point| {
|
||||
self.document
|
||||
.entities()
|
||||
.filter(|entity| entity.common().handle != dimension)
|
||||
.filter_map(|entity| {
|
||||
source_distance_squared(entity, point)
|
||||
.map(|distance| (distance, entity.common().handle))
|
||||
})
|
||||
.filter(|(distance, _)| *distance <= 1e-16)
|
||||
.min_by(|first, second| first.0.total_cmp(&second.0))
|
||||
.map(|(_, handle)| handle)
|
||||
})
|
||||
.filter(|(distance, _)| *distance <= 1e-16)
|
||||
.min_by(|first, second| first.0.total_cmp(&second.0))
|
||||
.map(|(_, handle)| handle))
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_associative_dimensions(
|
||||
@@ -970,7 +983,7 @@ impl Scene {
|
||||
if let Some(point) = resolved[3] {
|
||||
angular.definition_point = point;
|
||||
}
|
||||
angular.base.definition_point = angular.definition_point;
|
||||
angular.base.definition_point = angular.dimension_arc;
|
||||
}
|
||||
Dimension::Radius(radius) => {
|
||||
let Some((radial, angle)) = radial_source else {
|
||||
@@ -1137,9 +1150,21 @@ impl Scene {
|
||||
arc.base.actual_measurement = arc.measurement();
|
||||
}
|
||||
}
|
||||
dimension.base_mut().actual_measurement = dimension.measurement();
|
||||
let measurement = match &*dimension {
|
||||
Dimension::Linear(linear) => linear_measurement(linear),
|
||||
_ => dimension.measurement(),
|
||||
};
|
||||
dimension.base_mut().actual_measurement = measurement;
|
||||
refreshed.push((association.dimension, ChangeKind::Modified));
|
||||
}
|
||||
refreshed
|
||||
}
|
||||
}
|
||||
|
||||
fn linear_measurement(linear: &acadrust::entities::DimensionLinear) -> f64 {
|
||||
let plane = plane_from_normal(linear.first_point, linear.base.normal);
|
||||
let first = plane.project(dpoint(linear.first_point)).unwrap_or([0.0; 2]);
|
||||
let second = plane.project(dpoint(linear.second_point)).unwrap_or(first);
|
||||
let axis = [linear.rotation.cos(), linear.rotation.sin()];
|
||||
((second[0] - first[0]) * axis[0] + (second[1] - first[1]) * axis[1]).abs()
|
||||
}
|
||||
@@ -1420,6 +1420,11 @@ pub struct Scene {
|
||||
pub selection: Rc<RefCell<SelectionState>>,
|
||||
/// The CAD document — single source of truth for all entities.
|
||||
pub document: CadDocument,
|
||||
/// Last dimension created during this application session. This is
|
||||
/// intentionally not reconstructed from file handles: continuation and
|
||||
/// baseline commands must not treat an old, merely high-handle dimension
|
||||
/// loaded from disk as the user's most recently created dimension.
|
||||
pub last_created_dimension: Option<Handle>,
|
||||
/// File-open-prepared index for non-graphical semantic object lookups.
|
||||
pub(crate) object_data_cache: crate::entities::object_data::ObjectDataCache,
|
||||
/// Native AcDbLight/Sun inputs, separated by rendered block and viewport
|
||||
@@ -1848,6 +1853,7 @@ impl Scene {
|
||||
model_pane_min_px: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
|
||||
selection: Rc::new(RefCell::new(SelectionState::default())),
|
||||
document: CadDocument::new(),
|
||||
last_created_dimension: None,
|
||||
object_data_cache: crate::entities::object_data::ObjectDataCache::default(),
|
||||
lighting_cache: RefCell::new(HashMap::default()),
|
||||
selected: HashSet::default(),
|
||||
|
||||
Reference in new issue
Block a user