Merge remote-tracking branch 'fork/codex/polysolid-parity' into codex/polysolid-parity

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/scene/model/solid_history.rs
This commit is contained in:
ramox81 committed 2026-09-01 23:14:21 +03:00
commit 529ecd8cd6
28 files changed
+3047 -499

No files matched your search

+118 -116
View File
@@ -140,12 +140,16 @@ pub enum LocalSub {
Nested(NestedRef),
}
#[derive(Clone, Copy, Debug, Default)]
pub struct BlockMetrics {
pub aabb_local: [f32; 4],
pub has_point_marker: bool,
}
#[derive(Clone, Debug, Default)]
pub struct BlockDefn {
pub subs: Vec<LocalSub>,
pub has_point_marker: bool,
/// Union of direct and nested geometry in definition coordinates.
pub aabb_local: [f32; 4],
pub metrics: BlockMetrics,
/// Raw entity count of the source block record (`entity_handles.len()`).
/// Divisor for nested depth composition: a nested insert's children get a
/// sub-range of `parent_scale / (child_count + 1)`, shared with the scene
@@ -247,8 +251,8 @@ impl BlockCache {
// by-name references (`LocalSub::Nested`), never expanded here, so a
// block's build never depends on another block's defn. That makes the
// builds embarrassingly parallel over the read-only `doc` — no
// topological ordering required. `compute_block_aabbs` stays a serial
// post-pass (it resolves nested references and is comparatively cheap).
// topological ordering required. Metrics are resolved after all
// definitions exist.
cache.defns = referenced
.par_iter()
.map(|name| {
@@ -267,7 +271,7 @@ impl BlockCache {
)
})
.collect();
cache.compute_block_aabbs(&referenced);
cache.compute_block_metrics(&referenced);
cache
}
@@ -304,106 +308,73 @@ impl BlockCache {
)
})
.collect();
cache.compute_block_aabbs(&referenced);
cache.compute_block_metrics(&referenced);
cache
}
/// Compute and store the `aabb_local` for every cached defn. Direct wires
/// contribute their own aabb_local; nested INSERT references look up the
/// nested defn (already cached) and transform its aabb_local by the
/// nested Insert's transform before unioning.
///
/// Run as a post-pass so it doesn't matter which order build_defn was
/// called in. Cycle guard: a self-referential block keeps an empty AABB
/// (will fail every frustum test → not emitted, which is correct).
fn compute_block_aabbs(&mut self, names: &[String]) {
/// Resolve recursive bounds and point-marker presence.
fn compute_block_metrics(&mut self, names: &[String]) {
use crate::par::prelude::*;
// Phase 1 (parallel, read-only): each defn's union AABB is resolved
// by `defn_aabb_recursive`, which only *reads* `self.defns` (the map
// is fully built by now). There's no memoization, so a defn shared by
// many parents is re-walked per parent — real work on block-heavy
// drawings — and the per-name walks are independent, so they fan out.
// Definitions are complete, so each recursive read can run independently.
let this: &Self = self;
let resolved: Vec<(&String, [f32; 4], bool)> = names
let resolved: Vec<(&String, BlockMetrics)> = names
.par_iter()
.map(|name| {
let mut visited: Vec<String> = Vec::new();
let aabb = this.defn_aabb_recursive(name, &mut visited);
visited.clear();
let has_marker = this.defn_has_point_marker_recursive(name, &mut visited);
(name, aabb, has_marker)
(name, this.defn_metrics_recursive(name, &mut visited))
})
.collect();
// Phase 2 (serial): store the AABB back into each defn.
for (name, aabb, has_marker) in resolved {
for (name, metrics) in resolved {
if let Some(defn_arc) = self.defns.get_mut(name) {
let mut defn = (**defn_arc).clone();
defn.aabb_local = aabb;
defn.has_point_marker = has_marker;
defn.metrics = metrics;
*defn_arc = Arc::new(defn);
}
}
}
fn defn_has_point_marker_recursive(
fn defn_metrics_recursive(
&self,
block_name: &str,
visited: &mut Vec<String>,
) -> bool {
) -> BlockMetrics {
if visited.iter().any(|name| name == block_name) {
return false;
return BlockMetrics::default();
}
let Some(defn) = self.defns.get(block_name) else {
return false;
return BlockMetrics::default();
};
visited.push(block_name.to_string());
let found = defn.subs.iter().any(|sub| match sub {
LocalSub::Wire(wire) => wire.point_marker.is_some(),
LocalSub::Nested(insert) => {
insert
.attachments
.iter()
.any(|wire| wire.point_marker.is_some())
|| self.defn_has_point_marker_recursive(&insert.block_name, visited)
}
});
visited.pop();
found
}
/// Returns the union AABB for `block_name`'s defn, expressed in **that
/// defn's offset frame** (so its caller can store it in
/// `BlockDefn.aabb_local` without a coordinate-frame mismatch).
///
/// LocalWire contributions are already in the parent defn's offset
/// frame. Nested-INSERT contributions live in the *child* defn's offset
/// frame, so we re-add `child.local_offset` (f64), apply the nested
/// Insert's transform to get parent-native coordinates, then subtract
/// `parent.local_offset` to land back in the parent's offset frame.
fn defn_aabb_recursive(&self, block_name: &str, visited: &mut Vec<String>) -> [f32; 4] {
if visited.iter().any(|n| n == block_name) {
return [0.0, 0.0, 0.0, 0.0];
}
let Some(defn) = self.defns.get(block_name) else {
return [0.0, 0.0, 0.0, 0.0];
};
visited.push(block_name.to_string());
let mut acc = [0.0_f32, 0.0, 0.0, 0.0];
let mut metrics = BlockMetrics::default();
for sub in &defn.subs {
let aabb = match sub {
LocalSub::Wire(lw) => lw.aabb_local,
LocalSub::Nested(nref) => {
let nested = self.defn_aabb_recursive(&nref.block_name, visited);
nref.attachments.iter().fold(
transform_aabb_xy(nested, &nref.xform),
|aabb, wire| aabb_union(aabb, wire.aabb_local),
)
match sub {
LocalSub::Wire(wire) => {
metrics.aabb_local = aabb_union(metrics.aabb_local, wire.aabb_local);
metrics.has_point_marker |= wire.point_marker.is_some();
}
};
acc = aabb_union(acc, aabb);
LocalSub::Nested(nref) => {
let nested = self.defn_metrics_recursive(&nref.block_name, visited);
metrics.has_point_marker |= nested.has_point_marker;
for offset in &nref.instance_offsets {
let transform = nested_instance_transform(nref, *offset);
metrics.aabb_local = aabb_union(
metrics.aabb_local,
transform_aabb_xy(nested.aabb_local, &transform),
);
let attachment_transform = nested_attachment_transform(nref, *offset);
for wire in &nref.attachments {
metrics.aabb_local = aabb_union(
metrics.aabb_local,
transform_aabb_xy(wire.aabb_local, &attachment_transform),
);
metrics.has_point_marker |= wire.point_marker.is_some();
}
}
}
}
}
visited.pop();
acc
metrics
}
}
@@ -479,6 +450,21 @@ fn layer_hidden(doc: &CadDocument, layer: &str) -> bool {
.unwrap_or(false)
}
fn nested_instance_transform(nref: &NestedRef, offset: [f64; 3]) -> Transform {
if offset == [0.0; 3] {
nref.xform.clone()
} else {
Transform::from_translation(Vector3::new(offset[0], offset[1], offset[2]))
.then(&nref.xform)
}
}
fn nested_attachment_transform(nref: &NestedRef, offset: [f64; 3]) -> Transform {
let base = nref.xform.apply(Vector3::ZERO);
let instance = nested_instance_transform(nref, offset).apply(Vector3::ZERO);
Transform::from_translation(instance - base)
}
fn build_defn(
doc: &CadDocument,
block_name: &str,
@@ -549,6 +535,7 @@ fn build_defn(
for block_use in &block_uses {
let mut nested = build_nested_ref(
&block_use.insert,
block_use.scale_policy,
doc,
anno_scale,
bg_color,
@@ -597,14 +584,14 @@ fn build_defn(
}
BlockDefn {
subs,
has_point_marker: false,
aabb_local: [0.0; 4],
metrics: BlockMetrics::default(),
child_count: br.entity_handles.len(),
}
}
fn build_nested_ref(
nested_ins: &acadrust::entities::Insert,
scale_policy: crate::scene::BlockScalePolicy,
doc: &CadDocument,
anno_scale: f32,
bg_color: [f32; 4],
@@ -614,10 +601,11 @@ fn build_nested_ref(
// Bake the XCLIP boundary (parent-defn-local) so the nested insert keeps
// its clip when the parent block is expanded — the spatial filter object
// isn't reachable at expand time.
let xform = crate::scene::render_graph::insert_transform_at_scale(
let xform = crate::scene::render_graph::insert_transform_with_policy(
doc,
nested_ins,
anno_scale,
scale_policy,
);
let clip_poly = crate::scene::pick::xclip::insert_spatial_filter(doc, nested_ins)
.map(|filter| {
@@ -635,6 +623,7 @@ fn build_nested_ref(
doc,
nested_ins,
anno_scale,
scale_policy,
)
.into_iter()
.flat_map(|attribute| {
@@ -977,12 +966,18 @@ pub fn expand_insert(
// Current annotation scale. An annotative block scales as one uniform unit
// about its insertion point; a non-annotative block is unaffected.
anno_scale: f32,
scale_policy: crate::scene::BlockScalePolicy,
// Dimension picture blocks store definition POINTs at their root. They are
// metadata, not visible geometry; nested POINTs remain regular block data.
suppress_root_points: bool,
) -> Option<Vec<WireModel>> {
let defn = cache.defn(&ins.block_name)?;
let xform = crate::scene::render_graph::insert_transform_at_scale(doc, ins, anno_scale);
let xform = crate::scene::render_graph::insert_transform_with_policy(
doc,
ins,
anno_scale,
scale_policy,
);
let name = ins_handle.value().to_string();
let prototype_key = if !ins.is_array()
&& cache.prototype_blocks.contains(&ins.block_name)
@@ -1042,10 +1037,22 @@ pub fn expand_insert(
let mut batches = Batches::default();
let mut visited: Vec<String> = Vec::with_capacity(8);
// `defn.aabb_local` is in the defn's offset frame — re-add
// `defn.local_offset` (f64) before transforming so the world AABB is
// accurate for distant content.
let insert_world = transform_aabb_xy(defn.aabb_local, &xform);
let offsets = crate::scene::render_graph::array_offsets(ins);
let cell_xform = |offset: &[f64; 3]| {
crate::scene::render_graph::insert_instance_transform(
doc,
ins,
*offset,
anno_scale,
scale_policy,
)
};
let insert_world = offsets.iter().fold([0.0_f32; 4], |aabb, offset| {
aabb_union(
aabb,
transform_aabb_xy(defn.metrics.aabb_local, &cell_xform(offset)),
)
});
let insert_local = [
insert_world[0] as f32,
insert_world[1] as f32,
@@ -1062,7 +1069,7 @@ pub fn expand_insert(
// Whole-Insert pixel-size LOD: if the entire Insert footprint projects
// to sub-pixel size, skip it entirely.
if let Some(wpp) = world_per_pixel {
if !defn.has_point_marker && aabb_pixel_size(insert_local, wpp) < MIN_PIXEL_SIZE {
if !defn.metrics.has_point_marker && aabb_pixel_size(insert_local, wpp) < MIN_PIXEL_SIZE {
return Some(vec![]);
}
}
@@ -1085,16 +1092,7 @@ pub fn expand_insert(
is_xref,
bg_color,
};
let offsets = crate::scene::render_graph::array_offsets(ins);
if offsets.len() > 1 {
let cell_xform = |offset: &[f64; 3]| {
if offset == &[0.0; 3] {
xform.clone()
} else {
Transform::from_translation(Vector3::new(offset[0], offset[1], offset[2]))
.then(&xform)
}
};
let first_xform = cell_xform(&offsets[0]);
let first_translation = transform_translation(&first_xform);
let mut first_batches = Batches::default();
@@ -1135,14 +1133,7 @@ pub fn expand_insert(
}
for offset in &offsets {
let base_xform = if offset == &[0.0; 3] {
xform.clone()
} else {
let translation = Transform::from_translation(Vector3::new(
offset[0], offset[1], offset[2],
));
translation.then(&xform)
};
let base_xform = cell_xform(offset);
expand_defn(
defn,
&base_xform,
@@ -1816,8 +1807,18 @@ fn expand_defn(
is_xref: ctx.is_xref,
bg_color: ctx.bg_color,
};
for attachment in &nref.attachments {
emit_wire(attachment, accum_xform, &inner_ctx, out, d_range);
for offset in &nref.instance_offsets {
let attachment_transform = nested_attachment_transform(nref, *offset)
.then(accum_xform);
for attachment in &nref.attachments {
emit_wire(
attachment,
&attachment_transform,
&inner_ctx,
out,
d_range,
);
}
}
if visited.iter().any(|n| n == &nref.block_name) {
continue;
@@ -1825,8 +1826,17 @@ fn expand_defn(
let Some(nested_defn) = ctx.cache.defn(&nref.block_name) else {
continue;
};
let composed = nref.xform.then(accum_xform);
let world = transform_aabb_xy(nested_defn.aabb_local, &composed);
let world = nref.instance_offsets.iter().fold(
[0.0_f32; 4],
|aabb, offset| {
let composed = nested_instance_transform(nref, *offset)
.then(accum_xform);
aabb_union(
aabb,
transform_aabb_xy(nested_defn.metrics.aabb_local, &composed),
)
},
);
let local = [
world[0] as f32,
world[1] as f32,
@@ -1851,15 +1861,7 @@ fn expand_defn(
d_range.1 / (nested_defn.child_count.max(1) as f32 + 1.0),
);
let composed_for = |offset: &[f64; 3]| {
if offset == &[0.0; 3] {
nref.xform.then(accum_xform)
} else {
Transform::from_translation(Vector3::new(
offset[0], offset[1], offset[2],
))
.then(&nref.xform)
.then(accum_xform)
}
nested_instance_transform(nref, *offset).then(accum_xform)
};
if let Some(cp) = &nref.clip_poly {
let base_composed = nref.xform.then(accum_xform);
+2 -1
View File
@@ -107,10 +107,11 @@ fn block_entity_transform(
visited,
annotation_scale,
)?;
Some(inner.then(&crate::scene::render_graph::insert_transform_at_scale(
Some(inner.then(&crate::scene::render_graph::insert_transform_with_policy(
document,
&block_use.insert,
annotation_scale,
block_use.scale_policy,
)))
})
});
+7 -8
View File
@@ -144,8 +144,8 @@ pub fn symbol_radius(doc: &CadDocument, block: Handle, scale: f64) -> f64 {
) else {
return 0.0;
};
graph.walk_insert(
&block_use.insert,
graph.walk_block_use(
&block_use,
block,
|_, _| true,
|entity, context| {
@@ -322,16 +322,15 @@ pub fn place_block_wires(
) else {
return Vec::new();
};
let insert = &mut block_use.insert;
insert.set_x_scale(s);
insert.set_y_scale(s);
insert.set_z_scale(s);
block_use.insert.set_x_scale(s);
block_use.insert.set_y_scale(s);
block_use.insert.set_z_scale(s);
let depths = rustc_hash::FxHashMap::default();
let graph = crate::scene::render_graph::RenderSceneGraph::new(doc, None, None, true, &depths)
.with_annotation_scale(anno_scale);
let mut out = Vec::new();
graph.walk_insert(
insert,
graph.walk_block_use(
&block_use,
block,
|_, _| true,
|entity, context| {
+31 -16
View File
@@ -39,6 +39,7 @@ pub(crate) struct ExpandedBlockObject {
pub(crate) struct BlockObjectOptions {
pub(crate) suppress_root_points: bool,
pub(crate) apply_xclip: bool,
pub(crate) scale_policy: crate::scene::BlockScalePolicy,
}
fn block_object_style(
@@ -141,16 +142,18 @@ pub(crate) fn expand_block_object(
is_xref,
bg_color,
anno_scale,
options.scale_policy,
options.suppress_root_points,
)
.unwrap_or_default();
if options.apply_xclip {
if let Some(filter) = pick::xclip::insert_spatial_filter(document, insert) {
let transform = crate::scene::render_graph::insert_transform_at_scale(
let transform = crate::scene::render_graph::insert_transform_with_policy(
document,
insert,
anno_scale,
options.scale_policy,
);
let polygon = pick::xclip::world_clip_polygon_for_transform(filter, &transform);
pick::xclip::clip_wires(&mut wires, &polygon);
@@ -568,7 +571,7 @@ fn tessellate_entity_inner(
// True only when tessellating content shown inside a paper-space viewport.
// Retained through recursive INSERT expansion; PSLTSCALE itself is applied
// by the viewport's GPU uniform so it never changes resident wire content.
paper_space: bool,
_paper_space: bool,
) -> Vec<WireModel> {
let contextual = crate::scene::annotative::entity_for_annotation_context(
document,
@@ -980,6 +983,7 @@ fn tessellate_entity_inner(
pslt_factor,
BlockObjectOptions {
suppress_root_points: block_use.suppress_root_points,
scale_policy: block_use.scale_policy,
..BlockObjectOptions::default()
},
)
@@ -1087,7 +1091,10 @@ fn tessellate_entity_inner(
view_aabb,
world_per_pixel,
pslt_factor,
BlockObjectOptions::default(),
BlockObjectOptions {
scale_policy: block_use.scale_policy,
..BlockObjectOptions::default()
},
)
.wires;
if !wires.is_empty() {
@@ -1119,19 +1126,27 @@ fn tessellate_entity_inner(
.filter(|block_use| {
block_use.role == crate::scene::render_graph::BlockRole::TableCell
}) {
wires.extend(tessellate_entity(
document,
selected,
active_viewport,
bg_color,
1.0,
None,
&EntityType::Insert(block_use.insert),
block_cache,
view_aabb,
world_per_pixel,
paper_space,
));
wires.extend(
expand_block_object(
document,
&block_use.insert,
h,
sel,
active_viewport,
bg_color,
table_anno,
None,
block_cache,
view_aabb,
world_per_pixel,
pslt_factor,
BlockObjectOptions {
scale_policy: block_use.scale_policy,
..BlockObjectOptions::default()
},
)
.wires,
);
}
if !wires.is_empty() {
let aabb = entity_aabb(e);
+2 -2
View File
@@ -2100,8 +2100,8 @@ fn custom_arrow_from_block(
let mut lines = Vec::new();
let mut fill = Vec::new();
let mut deferred_hatch = false;
graph.walk_insert(
&block_use.insert,
graph.walk_block_use(
&block_use,
record.handle,
|_, _| true,
|entity, context| {
+1 -1
View File
@@ -1183,7 +1183,7 @@ impl Scene {
fn layer_plottable_in_context(
&self,
entity: &EntityType,
context: &crate::scene::render_graph::RenderContext,
context: &crate::scene::render_graph::InstanceContext,
) -> bool {
let common = entity.common();
let layer = if crate::scene::view::render::is_effective_layer_zero(&common.layer) {
+146 -16
View File
@@ -17,6 +17,13 @@ pub(crate) mod render_graph;
pub mod text;
pub mod view;
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
pub enum BlockScalePolicy {
#[default]
FromInsert,
Applied,
}
// Topic submodules split out of this root (each contributes `impl Scene`
// blocks and/or free functions). Pure text-move from the original mod.rs.
mod boundary;
@@ -66,6 +73,13 @@ struct DependencyTargets {
touches_block_definition: bool,
}
#[derive(Clone, Copy)]
enum DependencyKind {
Layer,
TextStyle,
DimStyle,
}
#[derive(Default)]
struct SceneDependencyIndex {
layers: HashMap<String, DependencyTargets>,
@@ -75,6 +89,7 @@ struct SceneDependencyIndex {
points: DependencyTargets,
text_geometry: DependencyTargets,
annotation_geometry: DependencyTargets,
signatures: HashMap<Handle, u64>,
}
fn hatch_interaction_aabb(hatch: &model::hatch_model::HatchModel) -> Option<[f64; 4]> {
@@ -164,6 +179,7 @@ use glam;
use iced::time::Duration;
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use std::cell::RefCell;
use std::hash::{Hash, Hasher};
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
@@ -2429,11 +2445,7 @@ impl Scene {
}
}
if !changes.is_empty() {
// A Modified delta can change layer/style/block references as well
// as coordinates. Rebuild the reverse dependency map lazily on its
// next use so later targeted global-property updates never follow
// stale ownership.
self.invalidate_dependency_index();
self.refresh_dependency_index_for_changes(&changes);
}
let cached_light_changed = self.lighting_cache.borrow().values().any(|lights| {
changes
@@ -2533,6 +2545,7 @@ impl Scene {
// Structural change — drop both per-entity tessellation memos.
self.tess_memo.borrow_mut().clear();
self.resident_tess_memo.borrow_mut().clear();
self.invalidate_dependency_index();
// Can't name the changed handles → force every journal consumer to rebuild.
self.push_geometry_delta(epoch, Vec::new(), true);
}
@@ -2616,6 +2629,7 @@ impl Scene {
let epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
self.geometry_epoch = epoch;
self.lighting_cache.borrow_mut().clear();
self.invalidate_dependency_index();
self.push_geometry_delta(epoch, Vec::new(), true);
}
@@ -7132,8 +7146,8 @@ impl Scene {
) {
continue;
}
graph.walk_insert(
&block_use.insert,
graph.walk_block_use(
&block_use,
common.handle,
|_, _| true,
|entity, context| {
@@ -7387,7 +7401,10 @@ impl Scene {
let changed_live: Vec<Handle> = changes
.iter()
.filter_map(|(handle, kind)| {
(!matches!(kind, ChangeKind::Removed)).then_some(*handle)
(!matches!(kind, ChangeKind::Removed)
&& !self.entity_temporarily_hidden(*handle)
&& self.document.get_entity(*handle).is_some())
.then_some(*handle)
})
.collect();
let wires = Arc::new(self.wire_models_for(&changed_live));
@@ -8847,6 +8864,113 @@ impl Scene {
})
}
fn entity_dependency_signature(&self, entity: &EntityType) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
let common = entity.common();
common.handle.hash(&mut hasher);
common.owner_handle.hash(&mut hasher);
normalize_name(&common.layer).hash(&mut hasher);
let category = match entity {
EntityType::Point(_) => 1_u8,
EntityType::Text(_)
| EntityType::MText(_)
| EntityType::AttributeDefinition(_)
| EntityType::AttributeEntity(_)
| EntityType::Dimension(_)
| EntityType::Leader(_)
| EntityType::Tolerance(_)
| EntityType::MultiLeader(_)
| EntityType::Table(_)
| EntityType::Shape(_) => 2,
EntityType::Hatch(_) => 3,
EntityType::Insert(insert) if !insert.attributes.is_empty() => 4,
_ => 0,
};
category.hash(&mut hasher);
crate::scene::annotative::is_annotative(&self.document, entity).hash(&mut hasher);
for block_use in render_graph::entity_block_uses(&self.document, entity, 1.0) {
block_use.block.hash(&mut hasher);
normalize_name(&block_use.insert.block_name).hash(&mut hasher);
block_use.role.hash(&mut hasher);
block_use.scale_policy.hash(&mut hasher);
block_use.active.hash(&mut hasher);
}
match entity {
EntityType::Text(text) => normalize_name(&text.style).hash(&mut hasher),
EntityType::MText(text) => normalize_name(&text.style).hash(&mut hasher),
EntityType::AttributeDefinition(attribute) => {
normalize_name(&attribute.text_style).hash(&mut hasher)
}
EntityType::AttributeEntity(attribute) => {
normalize_name(&attribute.text_style).hash(&mut hasher)
}
EntityType::Insert(insert) => {
for attribute in &insert.attributes {
normalize_name(&attribute.common.layer).hash(&mut hasher);
normalize_name(&attribute.text_style).hash(&mut hasher);
}
}
EntityType::Dimension(dimension) => {
normalize_name(&dimension.base().style_name).hash(&mut hasher)
}
EntityType::Leader(leader) => {
normalize_name(&leader.dimension_style).hash(&mut hasher)
}
EntityType::Tolerance(tolerance) => {
normalize_name(&tolerance.dimension_style_name).hash(&mut hasher)
}
EntityType::Table(table) => {
table.table_style_handle.hash(&mut hasher);
for row in &table.rows {
row.style
.as_ref()
.and_then(|style| style.text_style_handle)
.hash(&mut hasher);
for cell in &row.cells {
cell.style
.as_ref()
.and_then(|style| style.text_style_handle)
.hash(&mut hasher);
for content in &cell.contents {
content.text_style_handle.hash(&mut hasher);
}
}
}
}
EntityType::MultiLeader(leader) => {
leader.style_handle.hash(&mut hasher);
leader.text_style_handle.hash(&mut hasher);
leader.context.text_style_handle.hash(&mut hasher);
}
EntityType::MLine(line) => line.style_handle.hash(&mut hasher),
_ => {}
}
hasher.finish()
}
fn refresh_dependency_index_for_changes(&self, changes: &[(Handle, ChangeKind)]) {
let mut cache = self.dependency_index_cache.borrow_mut();
let Some(index) = cache.as_ref() else {
return;
};
let unchanged = changes.iter().all(|(handle, kind)| {
if !matches!(kind, ChangeKind::Modified) {
return false;
}
let Some(entity) = self.document.get_entity(*handle) else {
return false;
};
index.signatures.get(handle).copied()
== Some(self.entity_dependency_signature(entity))
});
if !unchanged {
cache.take();
}
}
fn rebuild_dependency_index(&self) -> SceneDependencyIndex {
let layout_blocks: HashSet<Handle> = self
.document
@@ -8928,6 +9052,9 @@ impl Scene {
let mut index = SceneDependencyIndex::default();
for entity in self.document.entities() {
let common = entity.common();
index
.signatures
.insert(common.handle, self.entity_dependency_signature(entity));
let owner = if common.owner_handle.is_null() {
membership
.get(&common.handle)
@@ -9102,17 +9229,20 @@ impl Scene {
index
}
fn dependency_targets(&self, kind: &str, names: &[String]) -> DependencyTargets {
fn dependency_targets(
&self,
kind: DependencyKind,
names: &[String],
) -> DependencyTargets {
if self.dependency_index_cache.borrow().is_none() {
*self.dependency_index_cache.borrow_mut() = Some(self.rebuild_dependency_index());
}
let cache = self.dependency_index_cache.borrow();
let index = cache.as_ref().unwrap();
let map = match kind {
"layer" => &index.layers,
"text" => &index.text_styles,
"dim" => &index.dim_styles,
_ => unreachable!(),
DependencyKind::Layer => &index.layers,
DependencyKind::TextStyle => &index.text_styles,
DependencyKind::DimStyle => &index.dim_styles,
};
let mut combined = DependencyTargets::default();
for name in names {
@@ -9148,7 +9278,7 @@ impl Scene {
}
pub fn invalidate_layer_dependencies(&mut self, names: &[String]) {
let targets = self.dependency_targets("layer", names);
let targets = self.dependency_targets(DependencyKind::Layer, names);
self.invalidate_dependency_targets(targets);
}
@@ -9157,7 +9287,7 @@ impl Scene {
}
pub fn invalidate_text_style_dependencies_many(&mut self, names: &[String]) {
let targets = self.dependency_targets("text", names);
let targets = self.dependency_targets(DependencyKind::TextStyle, names);
self.invalidate_dependency_targets(targets);
}
@@ -9166,7 +9296,7 @@ impl Scene {
}
pub fn invalidate_dim_style_dependencies_many(&mut self, names: &[String]) {
let targets = self.dependency_targets("dim", names);
let targets = self.dependency_targets(DependencyKind::DimStyle, names);
self.invalidate_dependency_targets(targets);
}
+434 -98
View File
@@ -21,7 +21,8 @@ pub const GRIP_INNER_RADIUS: usize = 10_006;
pub const GRIP_SIDES: usize = 10_007;
pub const GRIP_MAJOR_RADIUS: usize = 10_008;
pub const GRIP_MINOR_RADIUS: usize = 10_009;
pub const GRIP_POSITION: usize = 10_010;
pub const GRIP_TOP_RADIUS: usize = 10_010;
pub const GRIP_POSITION: usize = 10_011;
pub const GRIP_BOX_CORNER_FIRST: usize = 10_100;
pub const GRIP_BOX_FACE_X_MIN: usize = 10_110;
pub const GRIP_BOX_FACE_X_MAX: usize = 10_111;
@@ -54,6 +55,7 @@ pub const PROP_ROTATION: &str = "solid_history_rotation";
pub const PROP_PROFILE_ROTATION: &str = "solid_history_profile_rotation";
pub const PROP_TWIST_ALONG_PATH: &str = "solid_history_twist_along_path";
pub const PROP_SCALE_ALONG_PATH: &str = "solid_history_scale_along_path";
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";
@@ -103,6 +105,8 @@ pub fn has_specialized_primitive_properties(
| SolidHistoryOperation::Sphere(_)
| SolidHistoryOperation::Cone(_)
| SolidHistoryOperation::Cylinder(_)
| SolidHistoryOperation::Torus(_)
| SolidHistoryOperation::Pyramid(_)
| SolidHistoryOperation::Sweep(_)
)
)
@@ -127,6 +131,8 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3>
value.reference_point.z,
],
),
SolidHistoryOperation::Torus(value) => world_point(value.base.transform, [0.0; 3]),
SolidHistoryOperation::Pyramid(value) => world_point(value.base.transform, [0.0; 3]),
_ => None,
}
}
@@ -245,6 +251,75 @@ fn sweep_path_length(value: &SolidHistorySweep) -> Option<f64> {
(length.is_finite() && length >= 0.0).then_some(length)
}
fn torus_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
value: &SolidHistoryTorus,
) -> Vec<PropSection> {
let Some(position) = world_point(value.base.transform, [0.0; 3]) 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, _) = displayed_history_state(object_show_history, show_history_mode);
vec![
PropSection {
title: t!("Geometry").into_owned(),
props: vec![
Property {
label: t!("Solid type").into_owned(),
field: "solid_history_type",
value: PropValue::ReadOnly(t!("Torus").into_owned()),
},
crate::entities::common::edit_prop(
t!("Position X").as_ref(),
PROP_POSITION_X,
position.x,
),
crate::entities::common::edit_prop(
t!("Position Y").as_ref(),
PROP_POSITION_Y,
position.y,
),
crate::entities::common::edit_prop(
t!("Position Z").as_ref(),
PROP_POSITION_Z,
position.z,
),
history_prop(
t!("Torus radius").as_ref(),
PROP_MAJOR_RADIUS,
value.major_radius,
),
history_prop(
t!("Tube radius").as_ref(),
PROP_MINOR_RADIUS,
value.minor_radius,
),
],
},
PropSection {
title: t!("Solid History").into_owned(),
props: vec![
Property {
label: t!("History").into_owned(),
field: PROP_HISTORY,
value: PropValue::ReadOnly(
if record_history { "Record" } else { "None" }.to_string(),
),
},
Property {
label: t!("Show History").into_owned(),
field: PROP_SHOW_HISTORY,
value: PropValue::ReadOnly(
if show_history { "Yes" } else { "No" }.to_string(),
),
},
],
},
]
}
fn cone_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
@@ -491,6 +566,132 @@ fn cylinder_properties(
]
}
fn pyramid_is_inscribed(value: &SolidHistoryPyramid) -> bool {
value.operation_minor == -1
}
fn pyramid_apothem_factor(sides: i32) -> f64 {
(std::f64::consts::PI / sides.clamp(3, 32) as f64).cos()
}
fn pyramid_display_radius(value: &SolidHistoryPyramid, radius: f64) -> f64 {
if pyramid_is_inscribed(value) {
radius
} else {
radius * pyramid_apothem_factor(value.sides)
}
}
fn pyramid_display_rotation(value: &SolidHistoryPyramid) -> Option<f64> {
let vertex_rotation = matrix(value.base.transform)
.map(|matrix| matrix.x_axis.y.atan2(matrix.x_axis.x))?;
if pyramid_is_inscribed(value) {
Some(vertex_rotation)
} else {
Some(vertex_rotation + std::f64::consts::PI / value.sides.clamp(3, 32) as f64)
}
}
fn pyramid_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
value: &SolidHistoryPyramid,
) -> Vec<PropSection> {
let Some(position) = world_point(value.base.transform, [0.0; 3]) else {
return Vec::new();
};
let rotation = pyramid_display_rotation(value).unwrap_or(0.0);
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!("Pyramid").into_owned()),
},
crate::entities::common::edit_prop(
t!("Position X").as_ref(),
PROP_POSITION_X,
position.x,
),
crate::entities::common::edit_prop(
t!("Position Y").as_ref(),
PROP_POSITION_Y,
position.y,
),
crate::entities::common::edit_prop(
t!("Position Z").as_ref(),
PROP_POSITION_Z,
position.z,
),
Property {
label: t!("Type").into_owned(),
field: PROP_PYRAMID_TYPE,
value: PropValue::Choice {
selected: if pyramid_is_inscribed(value) {
"Inscribed"
} else {
"Circumscribed"
}
.to_string(),
options: vec!["Circumscribed".to_string(), "Inscribed".to_string()],
},
},
history_prop(
t!("Base radius").as_ref(),
PROP_BASE_RADIUS,
pyramid_display_radius(value, value.radius),
),
history_prop(
t!("Top radius").as_ref(),
PROP_TOP_RADIUS,
pyramid_display_radius(value, value.top_radius),
),
history_prop(t!("Sides").as_ref(), PROP_SIDES, value.sides),
Property {
label: t!("Rotation").into_owned(),
field: PROP_ROTATION,
value: PropValue::EditText(
crate::entities::common::format_direction(rotation),
),
},
history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height),
],
},
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 sphere_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
@@ -671,46 +872,21 @@ pub fn primitive_properties(
let Some(operation) = document.solid_history_operation(handle) else {
return Vec::new();
};
let props = match operation {
match operation {
SolidHistoryOperation::Box(value) => {
return rectangular_properties(document, handle, value, "Box")
rectangular_properties(document, handle, value, "Box")
}
SolidHistoryOperation::Wedge(value) => {
return rectangular_properties(document, handle, value, "Wedge")
rectangular_properties(document, handle, value, "Wedge")
}
SolidHistoryOperation::Sphere(value) => {
return sphere_properties(document, handle, value)
}
SolidHistoryOperation::Cone(value) => return cone_properties(document, handle, value),
SolidHistoryOperation::Cylinder(value) => {
return cylinder_properties(document, handle, value)
}
SolidHistoryOperation::Sweep(value) => {
return sweep_properties(document, handle, value)
}
SolidHistoryOperation::Torus(value) => vec![
history_prop(
t!("Outer Radius").as_ref(),
PROP_OUTER_RADIUS,
value.major_radius + value.minor_radius,
),
history_prop(
t!("Inner Radius").as_ref(),
PROP_INNER_RADIUS,
(value.major_radius - value.minor_radius).max(0.0),
),
],
SolidHistoryOperation::Pyramid(value) => vec![
history_prop(t!("Radius").as_ref(), PROP_RADIUS, value.radius),
history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height),
history_prop(t!("Sides").as_ref(), PROP_SIDES, value.sides),
],
_ => return Vec::new(),
};
vec![PropSection {
title: t!("Primitive").into_owned(),
props,
}]
SolidHistoryOperation::Sphere(value) => sphere_properties(document, handle, value),
SolidHistoryOperation::Cone(value) => cone_properties(document, handle, value),
SolidHistoryOperation::Cylinder(value) => cylinder_properties(document, handle, value),
SolidHistoryOperation::Pyramid(value) => pyramid_properties(document, handle, value),
SolidHistoryOperation::Torus(value) => torus_properties(document, handle, value),
SolidHistoryOperation::Sweep(value) => sweep_properties(document, handle, value),
_ => Vec::new(),
}
}
pub fn is_primitive_property(field: &str) -> bool {
@@ -739,6 +915,7 @@ pub fn is_primitive_property(field: &str) -> bool {
| PROP_PROFILE_ROTATION
| PROP_TWIST_ALONG_PATH
| PROP_SCALE_ALONG_PATH
| PROP_PYRAMID_TYPE
)
}
@@ -982,6 +1159,36 @@ fn apply_sweep_geometry_property(
}
}
fn apply_torus_position_property(
value: &mut SolidHistoryTorus,
field: &str,
text: &str,
) -> Option<bool> {
let axis = match field {
PROP_POSITION_X => 0,
PROP_POSITION_Y => 1,
PROP_POSITION_Z => 2,
_ => return None,
};
let target = crate::entities::common::parse_length(text)?;
if !target.is_finite() {
return Some(false);
}
let current = matrix(value.base.transform)?;
let origin = current.transform_point3(glam::DVec3::ZERO);
if !origin.is_finite() || (target - origin[axis]).abs() <= 1e-12 {
return Some(false);
}
let mut delta = glam::DVec3::ZERO;
delta[axis] = target - origin[axis];
let updated = glam::DMat4::from_translation(delta) * current;
if !updated.is_finite() || updated.determinant().abs() <= 1e-12 {
return Some(false);
}
value.base.transform = updated.to_cols_array();
Some(true)
}
fn canonicalize_cylinder_radii(value: &mut SolidHistoryCylinder) -> bool {
if value.minor_radius <= value.major_radius {
value.x_radius = value.major_radius;
@@ -997,6 +1204,120 @@ fn canonicalize_cylinder_radii(value: &mut SolidHistoryCylinder) -> bool {
true
}
fn apply_pyramid_geometry_property(
value: &mut SolidHistoryPyramid,
field: &str,
text: &str,
) -> Option<bool> {
if field == PROP_PYRAMID_TYPE {
let next_inscribed = if text.eq_ignore_ascii_case("Inscribed") {
true
} else if text.eq_ignore_ascii_case("Circumscribed") {
false
} else {
return Some(false);
};
let old_inscribed = pyramid_is_inscribed(value);
if next_inscribed == old_inscribed {
return Some(false);
}
let factor = pyramid_apothem_factor(value.sides);
if factor <= 1e-9 {
return Some(false);
}
let base_display = if old_inscribed { value.radius } else { value.radius * factor };
let top_display = if old_inscribed {
value.top_radius
} else {
value.top_radius * factor
};
let Some(current) = matrix(value.base.transform) else {
return Some(false);
};
value.radius = if next_inscribed { base_display } else { base_display / factor };
value.top_radius = if next_inscribed { top_display } else { top_display / factor };
let half = std::f64::consts::PI / value.sides.clamp(3, 32) as f64;
let delta = if next_inscribed { half } else { -half };
value.base.transform =
(current * glam::DMat4::from_rotation_z(delta)).to_cols_array();
value.operation_minor = if next_inscribed { -1 } else { 0 };
return Some(true);
}
if field == PROP_ROTATION {
let target = crate::entities::common::parse_direction(text)?;
if !target.is_finite() {
return Some(false);
}
let current = matrix(value.base.transform)?;
let projected = current.x_axis.truncate();
if projected.length_squared() <= 1e-12 {
return Some(false);
}
let current_angle = projected.y.atan2(projected.x);
let target_vertex_angle = if pyramid_is_inscribed(value) {
target
} else {
target - std::f64::consts::PI / value.sides.clamp(3, 32) as f64
};
let delta = (target_vertex_angle - current_angle + std::f64::consts::PI)
.rem_euclid(std::f64::consts::TAU)
- std::f64::consts::PI;
if delta.abs() <= 1e-12 {
return Some(false);
}
let updated = current * glam::DMat4::from_rotation_z(delta);
if !updated.is_finite() || updated.determinant().abs() <= 1e-12 {
return Some(false);
}
value.base.transform = updated.to_cols_array();
return Some(true);
}
let axis = match field {
PROP_POSITION_X => 0,
PROP_POSITION_Y => 1,
PROP_POSITION_Z => 2,
_ => return None,
};
let target = crate::entities::common::parse_length(text)?;
if !target.is_finite() {
return Some(false);
}
let current = matrix(value.base.transform)?;
let origin = current.transform_point3(glam::DVec3::ZERO);
if !origin.is_finite() || (target - origin[axis]).abs() <= 1e-12 {
return Some(false);
}
let mut delta = glam::DVec3::ZERO;
delta[axis] = target - origin[axis];
let updated = glam::DMat4::from_translation(delta) * current;
if !updated.is_finite() || updated.determinant().abs() <= 1e-12 {
return Some(false);
}
value.base.transform = updated.to_cols_array();
Some(true)
}
fn set_pyramid_sides(value: &mut SolidHistoryPyramid, sides: i32) -> bool {
if !(3..=32).contains(&sides) || sides == value.sides {
return false;
}
if !pyramid_is_inscribed(value) {
let Some(current) = matrix(value.base.transform) else {
return false;
};
let old_factor = pyramid_apothem_factor(value.sides);
let new_factor = pyramid_apothem_factor(sides);
value.radius = value.radius * old_factor / new_factor;
value.top_radius = value.top_radius * old_factor / new_factor;
let old_half = std::f64::consts::PI / value.sides.clamp(3, 32) as f64;
let new_half = std::f64::consts::PI / sides as f64;
value.base.transform =
(current * glam::DMat4::from_rotation_z(old_half - new_half)).to_cols_array();
}
value.sides = sides;
true
}
pub fn apply_primitive_property(
operation: &mut SolidHistoryOperation,
field: &str,
@@ -1031,6 +1352,16 @@ pub fn apply_primitive_property(
return applied;
}
}
if let SolidHistoryOperation::Torus(torus_value) = operation {
if let Some(applied) = apply_torus_position_property(torus_value, field, value) {
return applied;
}
}
if let SolidHistoryOperation::Pyramid(pyramid_value) = operation {
if let Some(applied) = apply_pyramid_geometry_property(pyramid_value, field, value) {
return applied;
}
}
let Some(number) = (if field == PROP_SIDES {
value.trim().parse::<f64>().ok()
} else {
@@ -1155,46 +1486,55 @@ pub fn apply_primitive_property(
}
value.radius = radius;
}
SolidHistoryOperation::Torus(value) => {
let outer = value.major_radius + value.minor_radius;
let inner = (value.major_radius - value.minor_radius).max(0.0);
match field {
PROP_OUTER_RADIUS => {
let Some(next_outer) = positive() else {
return false;
};
if next_outer <= inner {
return false;
}
value.major_radius = (next_outer + inner) * 0.5;
value.minor_radius = (next_outer - inner) * 0.5;
SolidHistoryOperation::Torus(value) => match field {
PROP_MAJOR_RADIUS => {
let Some(radius) = positive() else {
return false;
};
if (radius - value.major_radius).abs() <= 1e-12 {
return false;
}
PROP_INNER_RADIUS => {
let Some(next_inner) = positive() else {
return false;
};
if next_inner >= outer {
return false;
}
value.major_radius = (outer + next_inner) * 0.5;
value.minor_radius = (outer - next_inner) * 0.5;
}
_ => return false,
value.major_radius = radius;
}
}
PROP_MINOR_RADIUS => {
let Some(radius) = positive() else {
return false;
};
if (radius - value.minor_radius).abs() <= 1e-12 {
return false;
}
value.minor_radius = radius;
}
_ => return false,
},
SolidHistoryOperation::Pyramid(value) => match field {
PROP_RADIUS => value.radius = positive().unwrap_or(value.radius),
PROP_BASE_RADIUS => {
let Some(displayed) = positive() else {
return false;
};
value.radius = if pyramid_is_inscribed(value) {
displayed
} else {
displayed / pyramid_apothem_factor(value.sides)
};
}
PROP_TOP_RADIUS => {
if number < 0.0 {
return false;
}
value.top_radius = if pyramid_is_inscribed(value) {
number
} else {
number / pyramid_apothem_factor(value.sides)
};
}
PROP_HEIGHT => value.height = positive().unwrap_or(value.height),
PROP_SIDES => {
let rounded = number.round();
if (number - rounded).abs() > 1e-9 {
return false;
}
let sides = rounded as i32;
if !(3..=71).contains(&sides) {
return false;
}
value.sides = sides;
return set_pyramid_sides(value, rounded as i32);
}
_ => return false,
},
@@ -1467,22 +1807,18 @@ pub fn primitive_grips(
),
SolidHistoryOperation::Torus(value) => {
add(
GRIP_OUTER_RADIUS,
GRIP_MAJOR_RADIUS,
value.base.transform,
[value.major_radius + value.minor_radius, 0.0, 0.0],
[value.major_radius, 0.0, 0.0],
GripShape::Square,
None,
);
add(
GRIP_INNER_RADIUS,
GRIP_MINOR_RADIUS,
value.base.transform,
[
(value.major_radius - value.minor_radius).max(0.0),
0.0,
0.0,
],
[value.major_radius, 0.0, value.minor_radius],
GripShape::Square,
None,
Some([0.0, 0.0, 1.0]),
);
}
SolidHistoryOperation::Pyramid(value) => {
@@ -1493,6 +1829,13 @@ pub fn primitive_grips(
GripShape::Square,
None,
);
add(
GRIP_TOP_RADIUS,
value.base.transform,
[value.top_radius, 0.0, value.height],
GripShape::Square,
None,
);
add(
GRIP_HEIGHT,
value.base.transform,
@@ -1500,7 +1843,7 @@ pub fn primitive_grips(
GripShape::Square,
Some([0.0, 0.0, 1.0]),
);
let angle = (value.sides.clamp(3, 71) as f64 * 5.0).to_radians();
let angle = (value.sides.clamp(3, 32) as f64 * 5.0).to_radians();
add(
GRIP_SIDES,
value.base.transform,
@@ -1644,33 +1987,22 @@ pub fn apply_primitive_grip(
value.radius = local.length().max(1e-6);
}
SolidHistoryOperation::Torus(value) => match grip_id {
GRIP_OUTER_RADIUS => {
let outer = local.x.hypot(local.y).max(1e-6);
let inner = (value.major_radius - value.minor_radius).max(1e-6);
if outer <= inner {
return false;
}
value.major_radius = (outer + inner) * 0.5;
value.minor_radius = (outer - inner) * 0.5;
}
GRIP_INNER_RADIUS => {
let inner = local.x.hypot(local.y).max(1e-6);
let outer = value.major_radius + value.minor_radius;
if inner >= outer {
return false;
}
value.major_radius = (outer + inner) * 0.5;
value.minor_radius = (outer - inner) * 0.5;
}
GRIP_MAJOR_RADIUS => value.major_radius = local.x.hypot(local.y).max(1e-6),
GRIP_MINOR_RADIUS => value.minor_radius = local.z.abs().max(1e-6),
_ => return false,
},
SolidHistoryOperation::Pyramid(value) => match grip_id {
GRIP_RADIUS => value.radius = local.x.hypot(local.y).max(1e-6),
GRIP_TOP_RADIUS => {
value.top_radius = local.x.hypot(local.y).max(0.0);
}
GRIP_HEIGHT => value.height = local.z.max(1e-6),
GRIP_SIDES => {
let angle = local.y.atan2(local.x).rem_euclid(std::f64::consts::TAU);
value.sides = (angle.to_degrees() / 5.0).round() as i32;
value.sides = value.sides.clamp(3, 71);
let sides = ((angle.to_degrees() / 5.0).round() as i32).clamp(3, 32);
if !set_pyramid_sides(value, sides) {
return false;
}
}
_ => return false,
},
@@ -1810,15 +2142,19 @@ pub fn torus_op(
pub fn pyramid_op(
transform: [f64; 16],
radius: f64,
top_radius: f64,
height: f64,
sides: usize,
inscribed: bool,
) -> SolidHistoryOperation {
SolidHistoryOperation::Pyramid(SolidHistoryPyramid {
base: base(transform),
operation_major: 1,
operation_minor: if inscribed { -1 } else { 0 },
height,
sides: sides as i32,
radius,
top_radius,
..SolidHistoryPyramid::default()
})
}
+11
View File
@@ -82,6 +82,17 @@ pub fn pyramid_solid(center: [f64; 3], radius: f64, height: f64, sides: usize) -
brep::make::pyramid(center, radius, height, sides)
}
/// Solid pyramid or polygonal frustum on a regular polygon of `sides` corners.
pub fn pyramid_frustum_solid(
center: [f64; 3],
base_radius: f64,
top_radius: f64,
height: f64,
sides: usize,
) -> Option<Body> {
brep::make::pyramid_frustum(center, base_radius, top_radius, height, sides)
}
// ── Placement ───────────────────────────────────────────────────────────────
/// Moves a body by a rigid transform, given as three axes and an origin.
+2 -24
View File
@@ -181,29 +181,7 @@ fn embedded_path(entity: &EntityType) -> Option<EmbeddedEntity> {
EntityType::Circle(value) => Some(EmbeddedEntity::Circle(value.clone())),
EntityType::Ellipse(value) => Some(EmbeddedEntity::Ellipse(value.clone())),
EntityType::LwPolyline(value) => Some(EmbeddedEntity::LwPolyline(value.clone())),
EntityType::Spline(_) => {
let planar = entity_curve(entity)?;
let samples = planar.curve.tessellate_within(1e-4);
if samples.len() < 2 {
return None;
}
let mut path = LwPolyline::new();
path.vertices = samples
.into_iter()
.map(|point| LwVertex::new(Vector2::new(point[0], point[1])))
.collect();
path.is_closed = planar.curve.is_closed();
let plane = crate::command::WorkingPlane::new(
glam::DVec3::from_array(planar.plane.origin),
glam::DVec3::from_array(planar.plane.x_axis),
glam::DVec3::from_array(planar.plane.y_axis),
);
let EntityType::LwPolyline(path) = plane.place_entity(EntityType::LwPolyline(path))
else {
return None;
};
Some(EmbeddedEntity::LwPolyline(path))
}
EntityType::Spline(value) => Some(EmbeddedEntity::Spline(value.clone())),
_ => None,
}
}
@@ -231,7 +209,7 @@ pub fn polysolid(
+ Vec3::from(planar.plane.y_axis) * tangent_local.y)
.normalize()?;
let normal = Vec3::from(planar.plane.normal()?).normalize()?;
let side = tangent.cross(normal).normalize()?;
let side = normal.cross(tangent).normalize()?;
let offset = match justification {
PolysolidJustification::Left => 0.0,
PolysolidJustification::Center => -width * 0.5,
+38 -24
View File
@@ -343,11 +343,11 @@ pub struct MeshBatchChunk {
/// erase — the geometry behind them.
pub transp_index_buffer: wgpu::Buffer,
pub transp_index_count: u32,
/// Triangle-edge line list for plain meshes that carry no B-rep edges.
/// The dedicated vertex buffer matches the edge pipeline layout.
/// Expanded triangle-edge line list for plain meshes that carry no B-rep
/// edges. Keeping the edge endpoints together avoids coupling this buffer
/// to the surface mesh's indexing and vertex representation.
pub wire_vertex_buffer: wgpu::Buffer,
pub wire_index_buffer: wgpu::Buffer,
pub wire_index_count: u32,
pub wire_vertex_count: u32,
/// B-rep feature edges of ACIS solids, as a standalone LineList vertex
/// buffer (pairs of endpoints), drawn non-indexed. Empty for plain meshes.
pub edge_vertex_buffer: wgpu::Buffer,
@@ -438,7 +438,7 @@ fn make_chunk(
queue.write_buffer(&buffer, 0, bytemuck::cast_slice(data));
buffer
};
let compact_vertices = !material_has_textures(material) && wire_indices.is_empty();
let compact_vertices = !material_has_textures(material);
let mk_plain_vertex = |data: &[MeshVertex]| {
if data.is_empty() {
return stubs.vertex.clone();
@@ -479,18 +479,22 @@ fn make_chunk(
} else {
instances
};
let wire_vertices: Vec<MeshEdgeVertex> = if wire_indices.is_empty() {
Vec::new()
} else {
verts
.iter()
.map(|vertex| MeshEdgeVertex {
let mut wire_vertices = Vec::with_capacity(wire_indices.len());
for line in wire_indices.chunks_exact(2) {
let (Some(start), Some(end)) = (
verts.get(line[0] as usize),
verts.get(line[1] as usize),
) else {
continue;
};
for vertex in [start, end] {
wire_vertices.push(MeshEdgeVertex {
position: vertex.position,
color: face_color,
position_low: vertex.position_low,
})
.collect()
};
});
}
}
MeshBatchChunk {
vertex_buffer: vertex_buffer_override.unwrap_or_else(|| {
if compact_vertices {
@@ -505,8 +509,7 @@ fn make_chunk(
transp_index_buffer: mk_index(transp_indices, "mesh.batch.transp_ibuf"),
transp_index_count: transp_indices.len() as u32,
wire_vertex_buffer: mk_edge_vertex(&wire_vertices, "mesh.batch.wire_vbuf"),
wire_index_buffer: mk_index(wire_indices, "mesh.batch.wire_ibuf"),
wire_index_count: wire_indices.len() as u32,
wire_vertex_count: wire_vertices.len() as u32,
edge_vertex_buffer: edge_buffer_override
.unwrap_or_else(|| mk_edge_vertex(edge_verts, "mesh.batch.edge_vbuf")),
edge_vertex_count: edge_verts.len() as u32,
@@ -1169,8 +1172,7 @@ fn build_instanced_chunk(
let material = first.material;
let color = first.color;
let source_handle = source.handle.value();
let compact_vertices = !material_has_textures(material)
&& (!first.include_edges || !source.edge_verts.is_empty());
let compact_vertices = !material_has_textures(material);
let material_identity = if material_has_textures(material) {
material.map_or(0, |material| {
material
@@ -1582,7 +1584,10 @@ pub fn build_mesh_batch_filtered(
let budget = hard_budget.min(32 * 1024 * 1024);
let vsize = std::mem::size_of::<MeshVertex>();
let max_verts = (budget / vsize).max(3);
let max_tris = (budget / (6 * 4)).max(1); // wire-index buffer: 6 u32 per tri
// A triangle contributes three line segments (six standalone edge
// vertices). Bound chunks by the largest buffer produced for wire meshes.
let max_tris =
(budget / (6 * std::mem::size_of::<MeshEdgeVertex>())).max(1);
let stubs = MeshBatchStubs::new(device);
let mut chunks = Vec::new();
@@ -1796,18 +1801,27 @@ pub fn build_mesh_batch_filtered(
.len()
.saturating_mul(std::mem::size_of::<MeshVertex>())
<= hard_budget
&& part.indices.len().saturating_mul(2 * std::mem::size_of::<u32>())
&& part.indices.len().saturating_mul(std::mem::size_of::<u32>())
<= hard_budget
&& part
.set
.instance_source
.as_ref()
.is_some_and(|source| {
source
let feature_edges_fit = source
.edge_verts
.len()
.saturating_mul(std::mem::size_of::<MeshEdgeVertex>())
<= hard_budget
<= hard_budget;
let triangle_edges_fit = !part.include_edges
|| !source.edge_verts.is_empty()
|| part
.indices
.len()
.saturating_mul(2)
.saturating_mul(std::mem::size_of::<MeshEdgeVertex>())
<= hard_budget;
feature_edges_fit && triangle_edges_fit
})
&& part
.indices
@@ -2152,8 +2166,8 @@ pub fn build_mesh_batch_filtered(
continue;
}
// Flush when adding this mesh would overflow either the vertex buffer
// or the wire-index buffer.
// Flush when adding this mesh would overflow either the surface vertex
// buffer or the expanded wire-vertex buffer.
if !verts.is_empty()
&& (verts.len() + mesh.verts.len() > max_verts
|| wire_indices.len() / 6 + mesh_tris > max_tris)
+7 -31
View File
@@ -3750,7 +3750,7 @@ impl Pipeline {
// Four draw paths share this pass:
// - Solid: `mesh_pipeline` + triangle index buf.
// - Wireframe: `mesh_wireframe_pipeline` + the
// pre-built `wire_index_buffer`.
// pre-built expanded wire vertex buffer.
// - HiddenLine: depth prepass (`mesh_depth_pipeline`,
// writes Z, no colour) → wire overlay.
// - Solid+Edges: `mesh_pipeline` shaded fill → wire
@@ -3809,17 +3809,9 @@ impl Pipeline {
);
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
// Plain-mesh triangulation edges.
if c.wire_index_count != 0 {
if c.wire_vertex_count != 0 {
pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..));
pass.set_index_buffer(
c.wire_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
pass.draw_indexed(
0..c.wire_index_count,
0,
0..c.instance_count,
);
pass.draw(0..c.wire_vertex_count, 0..c.instance_count);
}
// ACIS solid B-rep feature edges (LineList, non-indexed).
if c.edge_vertex_count != 0 {
@@ -3846,17 +3838,9 @@ impl Pipeline {
&[],
);
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
if c.wire_index_count != 0 {
if c.wire_vertex_count != 0 {
pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..));
pass.set_index_buffer(
c.wire_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
pass.draw_indexed(
0..c.wire_index_count,
0,
0..c.instance_count,
);
pass.draw(0..c.wire_vertex_count, 0..c.instance_count);
}
if c.edge_vertex_count != 0 {
pass.set_vertex_buffer(0, c.edge_vertex_buffer.slice(..));
@@ -4002,17 +3986,9 @@ impl Pipeline {
&[],
);
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
if c.wire_index_count != 0 {
if c.wire_vertex_count != 0 {
pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..));
pass.set_index_buffer(
c.wire_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
pass.draw_indexed(
0..c.wire_index_count,
0,
0..c.instance_count,
);
pass.draw(0..c.wire_vertex_count, 0..c.instance_count);
}
if c.edge_vertex_count != 0 {
pass.set_vertex_buffer(0, c.edge_vertex_buffer.slice(..));
+165 -36
View File
@@ -15,6 +15,7 @@ use crate::scene::view::render::{
has_resolved_book_color, is_effective_layer_zero, layer_render_style_viewport,
render_style_for_block_sub_viewport, render_style_for_viewport, InheritStyle,
};
use crate::scene::BlockScalePolicy;
pub type ResolvedStyle = ([f32; 4], f32, [f32; 8], f32, u8);
@@ -216,7 +217,7 @@ fn layer_aci(document: &CadDocument, layer: &str) -> u8 {
}
#[derive(Clone, Debug)]
pub struct RenderContext {
pub struct InstanceContext {
pub transform: Transform,
pub root_handle: Handle,
pub parent_insert: Handle,
@@ -227,9 +228,10 @@ pub struct RenderContext {
pub depth_scale: f32,
pub nesting_depth: usize,
pub viewport: Option<Handle>,
pub scale_policy: BlockScalePolicy,
}
impl RenderContext {
impl InstanceContext {
fn direct(depth_base: f32, viewport: Option<Handle>) -> Self {
Self {
transform: Transform::identity(),
@@ -242,6 +244,7 @@ impl RenderContext {
depth_scale: 1.0,
nesting_depth: 0,
viewport,
scale_policy: BlockScalePolicy::FromInsert,
}
}
@@ -314,8 +317,8 @@ impl<'a> RenderSceneGraph<'a> {
/// false for an Insert removes its whole subtree.
pub fn walk_root<V, F>(&self, root: SceneRoot, mut visible: V, mut leaf: F)
where
V: FnMut(&EntityType, &RenderContext) -> bool,
F: FnMut(&EntityType, &RenderContext),
V: FnMut(&EntityType, &InstanceContext) -> bool,
F: FnMut(&EntityType, &InstanceContext),
{
let block = root.content_block();
let viewport = match root {
@@ -344,12 +347,18 @@ impl<'a> RenderSceneGraph<'a> {
.depths
.get(&handle.value())
.map_or(0.0, |depth| depth[0]);
let context = RenderContext::direct(direct_depth, viewport);
let context = InstanceContext::direct(direct_depth, viewport);
if !self.document_visible(entity) || !visible(entity, &context) {
continue;
}
if let EntityType::Insert(insert) = entity {
self.walk_insert_instances(insert, &context, &mut visible, &mut leaf);
self.walk_insert_instances(
insert,
BlockScalePolicy::FromInsert,
&context,
&mut visible,
&mut leaf,
);
} else {
leaf(entity, &context);
self.walk_owned_content(entity, &context, &mut visible, &mut leaf, &mut Vec::new());
@@ -359,15 +368,36 @@ impl<'a> RenderSceneGraph<'a> {
/// Walk one synthetic or document-owned Insert. Used by entity renderers
/// whose content is itself a block reference.
pub fn walk_insert<V, F>(
pub fn walk_block_use<V, F>(
&self,
insert: &Insert,
block_use: &BlockUse,
root_handle: Handle,
mut visible: V,
mut leaf: F,
)
where
V: FnMut(&EntityType, &InstanceContext) -> bool,
F: FnMut(&EntityType, &InstanceContext),
{
self.walk_insert_with_policy(
&block_use.insert,
root_handle,
block_use.scale_policy,
&mut visible,
&mut leaf,
);
}
fn walk_insert_with_policy<V, F>(
&self,
insert: &Insert,
root_handle: Handle,
scale_policy: BlockScalePolicy,
visible: &mut V,
leaf: &mut F,
) where
V: FnMut(&EntityType, &RenderContext) -> bool,
F: FnMut(&EntityType, &RenderContext),
V: FnMut(&EntityType, &InstanceContext) -> bool,
F: FnMut(&EntityType, &InstanceContext),
{
let mut root_insert = insert.clone();
root_insert.common.handle = root_handle;
@@ -376,9 +406,15 @@ impl<'a> RenderSceneGraph<'a> {
.depths
.get(&root_handle.value())
.map_or(0.0, |depth| depth[0]);
let context = RenderContext::direct(depth_base, self.viewport);
let context = InstanceContext::direct(depth_base, self.viewport);
if self.document_visible(&entity) && visible(&entity, &context) {
self.walk_insert_instances(&root_insert, &context, &mut visible, &mut leaf);
self.walk_insert_instances(
&root_insert,
scale_policy,
&context,
visible,
leaf,
);
}
}
@@ -386,12 +422,13 @@ impl<'a> RenderSceneGraph<'a> {
fn walk_insert_instances<V, F>(
&self,
insert: &Insert,
parent: &RenderContext,
scale_policy: BlockScalePolicy,
parent: &InstanceContext,
visible: &mut V,
leaf: &mut F,
) where
V: FnMut(&EntityType, &RenderContext) -> bool,
F: FnMut(&EntityType, &RenderContext),
V: FnMut(&EntityType, &InstanceContext) -> bool,
F: FnMut(&EntityType, &InstanceContext),
{
let insert_entity = EntityType::Insert(insert.clone());
let block_style = parent
@@ -424,6 +461,7 @@ impl<'a> RenderSceneGraph<'a> {
insert,
offset,
self.annotation_scale,
scale_policy,
);
let transform = local.then(&parent.transform);
let mut context = parent.clone();
@@ -435,6 +473,7 @@ impl<'a> RenderSceneGraph<'a> {
context.depth_base = depth_base;
context.depth_scale = depth_scale;
context.nesting_depth += 1;
context.scale_policy = scale_policy;
if let Some(filter) = crate::scene::pick::xclip::insert_spatial_filter(
self.document,
insert,
@@ -465,13 +504,13 @@ impl<'a> RenderSceneGraph<'a> {
fn walk_block<V, F>(
&self,
block_name: &str,
context: &RenderContext,
context: &InstanceContext,
visible: &mut V,
leaf: &mut F,
stack: &mut Vec<String>,
) where
V: FnMut(&EntityType, &RenderContext) -> bool,
F: FnMut(&EntityType, &RenderContext),
V: FnMut(&EntityType, &InstanceContext) -> bool,
F: FnMut(&EntityType, &InstanceContext),
{
if context.nesting_depth > 32 {
return;
@@ -504,7 +543,13 @@ impl<'a> RenderSceneGraph<'a> {
continue;
}
stack.push(nested.block_name.clone());
self.walk_insert_instances(nested, context, visible, leaf);
self.walk_insert_instances(
nested,
BlockScalePolicy::FromInsert,
context,
visible,
leaf,
);
stack.pop();
}
_ => {
@@ -518,13 +563,13 @@ impl<'a> RenderSceneGraph<'a> {
fn walk_owned_content<V, F>(
&self,
entity: &EntityType,
context: &RenderContext,
context: &InstanceContext,
visible: &mut V,
leaf: &mut F,
stack: &mut Vec<String>,
) where
V: FnMut(&EntityType, &RenderContext) -> bool,
F: FnMut(&EntityType, &RenderContext),
V: FnMut(&EntityType, &InstanceContext) -> bool,
F: FnMut(&EntityType, &InstanceContext),
{
for block_use in entity_render_block_uses(self.document, entity, self.annotation_scale)
.into_iter()
@@ -537,7 +582,13 @@ impl<'a> RenderSceneGraph<'a> {
continue;
}
stack.push(block_use.insert.block_name.clone());
self.walk_insert_instances(&block_use.insert, context, visible, leaf);
self.walk_insert_instances(
&block_use.insert,
block_use.scale_policy,
context,
visible,
leaf,
);
stack.pop();
}
}
@@ -595,9 +646,24 @@ pub fn insert_transform_at_scale(
document: &CadDocument,
insert: &Insert,
annotation_scale: f32,
) -> Transform {
insert_transform_with_policy(
document,
insert,
annotation_scale,
BlockScalePolicy::FromInsert,
)
}
pub fn insert_transform_with_policy(
document: &CadDocument,
insert: &Insert,
annotation_scale: f32,
scale_policy: BlockScalePolicy,
) -> Transform {
let mut transform = insert_transform(document, insert);
if (annotation_scale - 1.0).abs() > 1.0e-6
if scale_policy == BlockScalePolicy::FromInsert
&& (annotation_scale - 1.0).abs() > 1.0e-6
&& insert
.common
.extended_data
@@ -615,7 +681,7 @@ pub fn insert_transform_at_scale(
transform
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum BlockRole {
Insert,
DimensionPicture,
@@ -631,6 +697,7 @@ pub struct BlockUse {
pub block: Handle,
pub insert: Insert,
pub role: BlockRole,
pub scale_policy: BlockScalePolicy,
/// Whether this edge belongs to the active render representation. Inactive
/// edges remain dependencies so cache invalidation and purge stay complete.
pub active: bool,
@@ -654,6 +721,7 @@ fn block_use(
document: &CadDocument,
mut insert: Insert,
role: BlockRole,
scale_policy: BlockScalePolicy,
active: bool,
replaces_host_wire: bool,
suppress_root_points: bool,
@@ -667,6 +735,7 @@ fn block_use(
block,
insert,
role,
scale_policy,
active,
replaces_host_wire,
suppress_root_points,
@@ -687,6 +756,7 @@ pub fn block_use_from_handle(
document,
Insert::new(record.name.clone(), insertion),
role,
BlockScalePolicy::Applied,
true,
false,
false,
@@ -727,6 +797,7 @@ fn entity_owned_block_uses(
document,
insert.clone(),
BlockRole::Insert,
BlockScalePolicy::FromInsert,
true,
true,
false,
@@ -743,6 +814,7 @@ fn entity_owned_block_uses(
document,
insert,
BlockRole::DimensionPicture,
BlockScalePolicy::FromInsert,
picture,
picture,
true,
@@ -771,6 +843,7 @@ fn entity_owned_block_uses(
document,
insert,
BlockRole::TablePicture,
BlockScalePolicy::FromInsert,
true,
true,
false,
@@ -780,15 +853,12 @@ fn entity_owned_block_uses(
uses.extend(
crate::entities::table::block_cell_inserts(table, document, annotation_scale)
.into_iter()
.map(|mut insert| {
insert
.common
.extended_data
.remove_record("AcAnnotativeData");
.map(|insert| {
block_use(
document,
insert,
BlockRole::TableCell,
BlockScalePolicy::Applied,
cells_active,
false,
false,
@@ -799,15 +869,12 @@ fn entity_owned_block_uses(
}
EntityType::MultiLeader(multileader) => {
crate::entities::multileader::block_content_insert(document, multileader)
.map(|mut insert| {
insert
.common
.extended_data
.remove_record("AcAnnotativeData");
.map(|insert| {
vec![block_use(
document,
insert,
BlockRole::MultiLeaderContent,
BlockScalePolicy::Applied,
true,
false,
false,
@@ -997,8 +1064,9 @@ pub fn insert_instance_transform(
insert: &Insert,
offset: [f64; 3],
annotation_scale: f32,
scale_policy: BlockScalePolicy,
) -> Transform {
let transform = insert_transform_at_scale(document, insert, annotation_scale);
let transform = insert_transform_with_policy(document, insert, annotation_scale, scale_policy);
if offset == [0.0; 3] {
transform
} else {
@@ -1007,6 +1075,32 @@ pub fn insert_instance_transform(
}
}
pub fn insert_instance_translation_delta(
document: &CadDocument,
insert: &Insert,
offset: [f64; 3],
annotation_scale: f32,
scale_policy: BlockScalePolicy,
) -> Vector3 {
let base = insert_instance_transform(
document,
insert,
[0.0; 3],
annotation_scale,
scale_policy,
)
.apply(Vector3::ZERO);
let instance = insert_instance_transform(
document,
insert,
offset,
annotation_scale,
scale_policy,
)
.apply(Vector3::ZERO);
instance - base
}
pub fn block_contains_hatch(
document: &CadDocument,
block_name: &str,
@@ -1035,3 +1129,38 @@ pub fn block_contains_hatch(
memo.insert(key, contains);
contains
}
#[cfg(test)]
mod tests {
use super::*;
use acadrust::xdata::ExtendedDataRecord;
#[test]
fn applied_block_scale_skips_annotative_rescaling() {
let document = CadDocument::new();
let mut insert = Insert::new("BLOCK", Vector3::new(10.0, 20.0, 0.0));
insert
.common
.extended_data
.add_record(ExtendedDataRecord::new("AcAnnotativeData"));
let point = Vector3::new(1.0, 0.0, 0.0);
let from_insert = insert_transform_with_policy(
&document,
&insert,
2.0,
BlockScalePolicy::FromInsert,
)
.apply(point);
let applied = insert_transform_with_policy(
&document,
&insert,
2.0,
BlockScalePolicy::Applied,
)
.apply(point);
assert!((from_insert.x - 12.0).abs() < 1.0e-9);
assert!((applied.x - 11.0).abs() < 1.0e-9);
}
}