feat(scene): integrate DWG object data
This commit is contained in:
parent
eb23715605
commit
6fb68d1c7c
10 changed files with 3379 additions and 21 deletions
|
|
@ -90,8 +90,8 @@ lyon_tessellation = "1.0.20"
|
|||
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation"] }
|
||||
|
||||
[patch.crates-io]
|
||||
# Track standard DWG entity data used by the scene integration.
|
||||
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "7aaea11" }
|
||||
# Track standard DWG object relationships used by the scene integration.
|
||||
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "4afb27d" }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
# Native enables the plugin host runtime (out-of-process plugins).
|
||||
|
|
|
|||
|
|
@ -59,6 +59,12 @@ impl OpenCADStudio {
|
|||
/// Preserves UI state (open pickers, edit buffer) across refreshes.
|
||||
pub(super) fn refresh_properties(&mut self) {
|
||||
let i = self.active_tab;
|
||||
if !crate::entities::object_data::cache_is_prepared(
|
||||
&self.tabs[i].scene.object_data_cache,
|
||||
) {
|
||||
self.tabs[i].scene.object_data_cache =
|
||||
crate::entities::object_data::build_cache(&self.tabs[i].scene.document);
|
||||
}
|
||||
// Note: the color-picker dropdown is intentionally NOT carried over — a
|
||||
// rebuild means the selection (or a property) changed, so the dropdown
|
||||
// closes, matching the deselect / reselect / click-away expectation.
|
||||
|
|
@ -135,7 +141,27 @@ impl OpenCADStudio {
|
|||
let new_panel = {
|
||||
let selected = self.tabs[i].scene.selected_entities();
|
||||
let mut panel = match selected.len() {
|
||||
0 => ui::PropertiesPanel::empty(),
|
||||
0 => {
|
||||
let sections = crate::entities::object_data::cached_document_sections(
|
||||
&self.tabs[i].scene.object_data_cache,
|
||||
);
|
||||
ui::PropertiesPanel {
|
||||
title: "Drawing".to_string(),
|
||||
sections,
|
||||
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
|
||||
linetype_combo: iced::widget::combo_box::State::new(
|
||||
linetype_items.clone(),
|
||||
),
|
||||
lineweight_combo: iced::widget::combo_box::State::new(
|
||||
ui::properties::lw_options(),
|
||||
),
|
||||
hatch_pattern_combo: iced::widget::combo_box::State::new(
|
||||
crate::scene::model::hatch_patterns::names(),
|
||||
),
|
||||
linetype_items,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
let (handle, source_entity) = selected[0];
|
||||
let contextual = crate::scene::annotative::entity_for_active_context(
|
||||
|
|
@ -514,6 +540,13 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
|
||||
sections.extend(crate::entities::object_data::sections(
|
||||
&self.tabs[i].scene.document,
|
||||
&self.tabs[i].scene.object_data_cache,
|
||||
handle,
|
||||
entity,
|
||||
));
|
||||
|
||||
// Uniform-scale checkbox for block references (#427):
|
||||
// while the three scale factors are equal (and the user
|
||||
// hasn't opted into per-axis editing) the panel shows one
|
||||
|
|
|
|||
|
|
@ -542,6 +542,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
self.tabs[i].scene.images = caches.images;
|
||||
self.tabs[i].scene.meshes = caches.meshes;
|
||||
self.tabs[i].scene.block_meshes = caches.block_meshes;
|
||||
self.tabs[i].scene.object_data_cache = caches.object_data;
|
||||
let prepared_geometry = caches.prepared_geometry.take();
|
||||
// Invalidate the wire cache so the new document is tessellated.
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
|
|
|
|||
|
|
@ -13,10 +13,12 @@ use acadrust::entities::Light;
|
|||
use acadrust::EntityType;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{center_grip, edit_prop as edit, ro_prop, square_grip};
|
||||
use crate::entities::common::{
|
||||
center_grip, edit_angle_prop, edit_prop as edit, ro_prop, square_grip,
|
||||
};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::scene::model::wire_model::SnapHint;
|
||||
|
||||
/// On-screen glyph radius in pixels for the screen-relative path.
|
||||
|
|
@ -271,13 +273,189 @@ impl PropertyEditable for Light {
|
|||
props.push(edit("Target Y", "li_ty", self.target.y));
|
||||
props.push(edit("Target Z", "li_tz", self.target.z));
|
||||
}
|
||||
vec![PropSection {
|
||||
let mut sections = vec![
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props,
|
||||
}]
|
||||
},
|
||||
PropSection {
|
||||
title: "Light".into(),
|
||||
props: vec![
|
||||
Property {
|
||||
label: "On".into(),
|
||||
field: "li_status",
|
||||
value: PropValue::BoolToggle {
|
||||
field: "li_status",
|
||||
value: self.status,
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: "Plot Glyph".into(),
|
||||
field: "li_plot_glyph",
|
||||
value: PropValue::BoolToggle {
|
||||
field: "li_plot_glyph",
|
||||
value: self.plot_glyph,
|
||||
},
|
||||
},
|
||||
ro_prop("Color", "li_color", format!("{:?}", self.light_color)),
|
||||
edit("Intensity", "li_intensity", self.intensity),
|
||||
Property {
|
||||
label: "Attenuation".into(),
|
||||
field: "li_attenuation",
|
||||
value: PropValue::Choice {
|
||||
selected: match self.attenuation_type {
|
||||
1 => "Inverse Linear",
|
||||
2 => "Inverse Square",
|
||||
_ => "None",
|
||||
}
|
||||
.to_string(),
|
||||
options: vec![
|
||||
"None".to_string(),
|
||||
"Inverse Linear".to_string(),
|
||||
"Inverse Square".to_string(),
|
||||
],
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: "Use Limits".into(),
|
||||
field: "li_use_limits",
|
||||
value: PropValue::BoolToggle {
|
||||
field: "li_use_limits",
|
||||
value: self.use_attenuation_limits,
|
||||
},
|
||||
},
|
||||
edit("Start Limit", "li_start", self.attenuation_start_limit),
|
||||
edit("End Limit", "li_end", self.attenuation_end_limit),
|
||||
edit_angle_prop(
|
||||
"Hotspot Angle",
|
||||
"li_hotspot",
|
||||
self.hotspot_angle.to_degrees(),
|
||||
),
|
||||
edit_angle_prop(
|
||||
"Falloff Angle",
|
||||
"li_falloff",
|
||||
self.falloff_angle.to_degrees(),
|
||||
),
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
title: "Shadows".into(),
|
||||
props: vec![
|
||||
Property {
|
||||
label: "Cast Shadows".into(),
|
||||
field: "li_shadows",
|
||||
value: PropValue::BoolToggle {
|
||||
field: "li_shadows",
|
||||
value: self.cast_shadows,
|
||||
},
|
||||
},
|
||||
ro_prop("Type", "li_shadow_type", self.shadow_type.to_string()),
|
||||
ro_prop("Map Size", "li_shadow_size", self.shadow_map_size.to_string()),
|
||||
ro_prop(
|
||||
"Softness",
|
||||
"li_shadow_softness",
|
||||
self.shadow_map_softness.to_string(),
|
||||
),
|
||||
],
|
||||
},
|
||||
];
|
||||
if let Some(photo) = &self.photometric_data {
|
||||
sections.push(PropSection {
|
||||
title: "Photometric".into(),
|
||||
props: vec![
|
||||
ro_prop("Web File", "li_web_file", photo.web_file.clone()),
|
||||
ro_prop(
|
||||
"Physical Method",
|
||||
"li_physical_method",
|
||||
photo.physical_intensity_method.to_string(),
|
||||
),
|
||||
edit(
|
||||
"Physical Intensity",
|
||||
"li_physical_intensity",
|
||||
photo.physical_intensity,
|
||||
),
|
||||
edit(
|
||||
"Illuminance Distance",
|
||||
"li_illuminance_distance",
|
||||
photo.illuminance_distance,
|
||||
),
|
||||
edit(
|
||||
"Color Temperature",
|
||||
"li_color_temperature",
|
||||
photo.lamp_color_temperature,
|
||||
),
|
||||
ro_prop(
|
||||
"Lamp Preset",
|
||||
"li_lamp_preset",
|
||||
photo.lamp_color_preset.to_string(),
|
||||
),
|
||||
ro_prop(
|
||||
"Shape",
|
||||
"li_shape",
|
||||
format!(
|
||||
"{} · {:.6} × {:.6} · radius {:.6}",
|
||||
photo.extended_light_shape,
|
||||
photo.extended_light_length,
|
||||
photo.extended_light_width,
|
||||
photo.extended_light_radius
|
||||
),
|
||||
),
|
||||
ro_prop(
|
||||
"Web",
|
||||
"li_web",
|
||||
format!(
|
||||
"type {}; symmetry {}; flux {:.6}; angles {:?}; rotation {:.6},{:.6},{:.6}",
|
||||
photo.web_file_type,
|
||||
photo.web_symmetry,
|
||||
photo.web_flux,
|
||||
photo.web_angles,
|
||||
photo.web_rotation.x,
|
||||
photo.web_rotation.y,
|
||||
photo.web_rotation.z
|
||||
),
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
sections
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
match field {
|
||||
"li_status" => {
|
||||
if let Ok(value) = value.parse::<bool>() {
|
||||
self.status = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
"li_plot_glyph" => {
|
||||
if let Ok(value) = value.parse::<bool>() {
|
||||
self.plot_glyph = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
"li_use_limits" => {
|
||||
if let Ok(value) = value.parse::<bool>() {
|
||||
self.use_attenuation_limits = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
"li_shadows" => {
|
||||
if let Ok(value) = value.parse::<bool>() {
|
||||
self.cast_shadows = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
"li_attenuation" => {
|
||||
self.attenuation_type = match value {
|
||||
"Inverse Linear" => 1,
|
||||
"Inverse Square" => 2,
|
||||
_ => 0,
|
||||
};
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
|
|
@ -288,6 +466,26 @@ impl PropertyEditable for Light {
|
|||
"li_tx" => self.target.x = v,
|
||||
"li_ty" => self.target.y = v,
|
||||
"li_tz" => self.target.z = v,
|
||||
"li_intensity" => self.intensity = v.max(0.0),
|
||||
"li_start" => self.attenuation_start_limit = v,
|
||||
"li_end" => self.attenuation_end_limit = v,
|
||||
"li_hotspot" => self.hotspot_angle = v.to_radians(),
|
||||
"li_falloff" => self.falloff_angle = v.to_radians(),
|
||||
"li_physical_intensity" => {
|
||||
if let Some(photo) = self.photometric_data.as_mut() {
|
||||
photo.physical_intensity = v;
|
||||
}
|
||||
}
|
||||
"li_illuminance_distance" => {
|
||||
if let Some(photo) = self.photometric_data.as_mut() {
|
||||
photo.illuminance_distance = v;
|
||||
}
|
||||
}
|
||||
"li_color_temperature" => {
|
||||
if let Some(photo) = self.photometric_data.as_mut() {
|
||||
photo.lamp_color_temperature = v;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub mod mline;
|
|||
pub mod mtext;
|
||||
pub mod multileader;
|
||||
pub mod names;
|
||||
pub mod object_data;
|
||||
pub mod ole2frame;
|
||||
pub mod point;
|
||||
pub mod polyline;
|
||||
|
|
|
|||
2725
src/entities/object_data.rs
Normal file
2725
src/entities/object_data.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -385,6 +385,10 @@ pub struct DerivedCaches {
|
|||
pub meshes: HashMap<Handle, MeshLodSet>,
|
||||
/// Block-definition solid meshes, block-local frame (instanced per INSERT). (#123)
|
||||
pub block_meshes: HashMap<Handle, MeshLodSet>,
|
||||
/// Non-graphical DWG object relationships and Drawing-property rows.
|
||||
/// Prepared during file open so entity selection/deselection never scans
|
||||
/// the complete object store on the UI thread.
|
||||
pub object_data: crate::entities::object_data::ObjectDataCache,
|
||||
/// Number of entities removed by the corrupt-entity guard during load.
|
||||
/// Reported back to the UI so the user knows when a file had parser-junk
|
||||
/// entities silently dropped.
|
||||
|
|
@ -441,6 +445,7 @@ fn build_derived_caches_impl(
|
|||
// the memoised set so each reference re-reads / re-fetches once here (and
|
||||
// stays cached across this document's later cache rebuilds).
|
||||
crate::scene::model::image_model::clear_image_cache();
|
||||
let object_data = crate::entities::object_data::build_cache(doc);
|
||||
// model-space block handle (same logic as Scene::model_space_block_handle)
|
||||
let model_block = doc
|
||||
.objects
|
||||
|
|
@ -665,6 +670,7 @@ fn build_derived_caches_impl(
|
|||
images,
|
||||
meshes,
|
||||
block_meshes,
|
||||
object_data,
|
||||
corrupt_dropped: 0,
|
||||
xref_dropped: 0,
|
||||
xrefs: Vec::new(),
|
||||
|
|
@ -1279,6 +1285,23 @@ struct BlockMeshInherit {
|
|||
layer0_material: crate::scene::model::material_model::MeshMaterial,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SceneLight {
|
||||
handle: Handle,
|
||||
/// Present when `color` must follow the current ByLayer color.
|
||||
color_layer: Option<String>,
|
||||
light_type: f32,
|
||||
position: [f64; 3],
|
||||
direction: [f32; 3],
|
||||
color: [f32; 3],
|
||||
intensity: f32,
|
||||
hotspot_cos: f32,
|
||||
falloff_cos: f32,
|
||||
attenuation_type: f32,
|
||||
attenuation_start: f32,
|
||||
attenuation_end: f32,
|
||||
}
|
||||
|
||||
pub struct Scene {
|
||||
pub camera: Rc<RefCell<Camera>>,
|
||||
/// Model-space tiled viewport layout. One full-window tile by default;
|
||||
|
|
@ -1325,6 +1348,12 @@ pub struct Scene {
|
|||
pub selection: Rc<RefCell<SelectionState>>,
|
||||
/// The CAD document — single source of truth for all entities.
|
||||
pub document: CadDocument,
|
||||
/// File-open-prepared index for non-graphical semantic object lookups.
|
||||
pub(crate) object_data_cache: crate::entities::object_data::ObjectDataCache,
|
||||
/// Native AcDbLight/Sun inputs. Built once after document load, then
|
||||
/// invalidated only when a light entity changes; ordinary geometry edits
|
||||
/// never rescan a million-entity drawing just to rediscover its lights.
|
||||
lighting_cache: RefCell<Option<Vec<SceneLight>>>,
|
||||
/// Currently selected entity handles.
|
||||
pub selected: HashSet<Handle>,
|
||||
/// Entity handles hidden by Isolate / Hide. Empty = nothing hidden.
|
||||
|
|
@ -1716,6 +1745,8 @@ impl Scene {
|
|||
model_panes: iced::widget::pane_grid::State::new(0).0,
|
||||
selection: Rc::new(RefCell::new(SelectionState::default())),
|
||||
document: CadDocument::new(),
|
||||
object_data_cache: crate::entities::object_data::ObjectDataCache::default(),
|
||||
lighting_cache: RefCell::new(None),
|
||||
selected: HashSet::default(),
|
||||
hidden: HashSet::default(),
|
||||
refedit_keep: None,
|
||||
|
|
@ -2093,6 +2124,16 @@ impl Scene {
|
|||
entities: &[(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)],
|
||||
undo: bool,
|
||||
) -> Vec<(Handle, ChangeKind)> {
|
||||
if entities.iter().any(|(_, before, after)| {
|
||||
before
|
||||
.as_deref()
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Light(_)))
|
||||
|| after
|
||||
.as_deref()
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Light(_)))
|
||||
}) {
|
||||
*self.lighting_cache.borrow_mut() = None;
|
||||
}
|
||||
let mut changes: Vec<(Handle, ChangeKind)> = Vec::with_capacity(entities.len());
|
||||
for (h, before, after) in entities {
|
||||
let target = if undo { before } else { after };
|
||||
|
|
@ -2127,6 +2168,30 @@ impl Scene {
|
|||
}
|
||||
|
||||
pub fn bump_entities(&mut self, changes: &[(Handle, ChangeKind)]) {
|
||||
let cached_light_changed = self.lighting_cache.borrow().as_ref().is_some_and(|lights| {
|
||||
changes
|
||||
.iter()
|
||||
.any(|(handle, _)| lights.iter().any(|light| light.handle == *handle))
|
||||
});
|
||||
let live_light_changed = changes.iter().any(|(handle, _)| {
|
||||
self.document
|
||||
.get_entity(*handle)
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Light(_)))
|
||||
});
|
||||
if cached_light_changed || live_light_changed {
|
||||
for (handle, _) in changes {
|
||||
let exists = self
|
||||
.document
|
||||
.get_entity(*handle)
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Light(_)));
|
||||
crate::entities::object_data::update_light_entity(
|
||||
&mut self.object_data_cache,
|
||||
*handle,
|
||||
exists,
|
||||
);
|
||||
}
|
||||
*self.lighting_cache.borrow_mut() = None;
|
||||
}
|
||||
let epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
|
||||
self.geometry_epoch = epoch;
|
||||
{
|
||||
|
|
|
|||
|
|
@ -40,6 +40,21 @@ pub struct Uniforms {
|
|||
pub _pad_eh: f32,
|
||||
pub eye_low: [f32; 3],
|
||||
pub _pad_el: f32,
|
||||
|
||||
// Up to four native AcDbLight/Sun sources. Positions are uploaded relative
|
||||
// to the current eye, preserving large-coordinate precision without
|
||||
// changing mesh buffers.
|
||||
/// xyz = eye-relative source position, w = light type (1 distant, 2 point,
|
||||
/// 3 spot).
|
||||
pub light_position_type: [[f32; 4]; 4],
|
||||
/// xyz = source-to-target direction, w = intensity.
|
||||
pub light_direction_intensity: [[f32; 4]; 4],
|
||||
/// rgb = source colour, w = cosine of hotspot angle.
|
||||
pub light_color_hotspot: [[f32; 4]; 4],
|
||||
/// x = attenuation mode, y/z = start/end limits, w = cosine of falloff.
|
||||
pub light_attenuation: [[f32; 4]; 4],
|
||||
/// x = active light count, y = fallback ambient strength.
|
||||
pub lighting: [f32; 4],
|
||||
}
|
||||
|
||||
impl Uniforms {
|
||||
|
|
@ -64,6 +79,11 @@ impl Uniforms {
|
|||
_pad_eh: 0.0,
|
||||
eye_low,
|
||||
_pad_el: 0.0,
|
||||
light_position_type: [[0.0; 4]; 4],
|
||||
light_direction_intensity: [[0.0; 4]; 4],
|
||||
light_color_hotspot: [[0.0; 4]; 4],
|
||||
light_attenuation: [[0.0; 4]; 4],
|
||||
lighting: [0.0, 0.18, 0.0, 0.0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ use crate::scene::pipeline::viewcube::{hover_id, VIEWCUBE_PX};
|
|||
use crate::scene::pipeline::MultiPipeline;
|
||||
use crate::scene::convert::tess_util;
|
||||
use crate::scene::{
|
||||
vp_effective_scale, HatchModel, ImageModel, MeshLodSet, NavPerfSample, Scene, Uniforms,
|
||||
ViewportInstance, WireModel,
|
||||
vp_effective_scale, HatchModel, ImageModel, MeshLodSet, NavPerfSample, Scene, SceneLight,
|
||||
Uniforms, ViewportInstance, WireModel,
|
||||
};
|
||||
|
||||
// ── Camera hover state (shader::Program::State) ───────────────────────────
|
||||
|
|
@ -1077,6 +1077,260 @@ fn crop_view_proj(view_proj: glam::Mat4, uo: f32, vo: f32, us: f32, vs: f32) ->
|
|||
// ── Render-style helpers (impl Scene) ────────────────────────────────────
|
||||
|
||||
impl Scene {
|
||||
fn build_lighting_cache(&self) -> Vec<SceneLight> {
|
||||
use acadrust::objects::{ClassObjectData, ObjectType};
|
||||
|
||||
fn normalized(value: [f64; 3], fallback: [f32; 3]) -> [f32; 3] {
|
||||
let length =
|
||||
(value[0] * value[0] + value[1] * value[1] + value[2] * value[2]).sqrt();
|
||||
if length <= 1e-12 {
|
||||
fallback
|
||||
} else {
|
||||
[
|
||||
(value[0] / length) as f32,
|
||||
(value[1] / length) as f32,
|
||||
(value[2] / length) as f32,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn solar_direction(
|
||||
sun: &acadrust::objects::Sun,
|
||||
geo: &acadrust::objects::GeoData,
|
||||
) -> Option<[f32; 3]> {
|
||||
if sun.julian_day < 1_000_000 {
|
||||
return None;
|
||||
}
|
||||
let daylight_ms = if sun.is_daylight_savings_on {
|
||||
3_600_000.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let jd = sun.julian_day as f64
|
||||
+ (sun.milliseconds as f64 - daylight_ms) / 86_400_000.0;
|
||||
let days = jd - 2_451_545.0;
|
||||
let mean_longitude = (280.460 + 0.985_647_4 * days).to_radians();
|
||||
let mean_anomaly = (357.528 + 0.985_600_3 * days).to_radians();
|
||||
let ecliptic_longitude = mean_longitude
|
||||
+ (1.915 * mean_anomaly.sin()
|
||||
+ 0.020 * (2.0 * mean_anomaly).sin())
|
||||
.to_radians();
|
||||
let obliquity = (23.439 - 0.000_000_4 * days).to_radians();
|
||||
let right_ascension = (obliquity.cos() * ecliptic_longitude.sin())
|
||||
.atan2(ecliptic_longitude.cos());
|
||||
let declination =
|
||||
(obliquity.sin() * ecliptic_longitude.sin()).asin();
|
||||
let local_sidereal = (280.460_618_37
|
||||
+ 360.985_647_366_29 * days
|
||||
+ geo.reference_point.x)
|
||||
.to_radians();
|
||||
let hour_angle = (local_sidereal - right_ascension + std::f64::consts::PI)
|
||||
.rem_euclid(std::f64::consts::TAU)
|
||||
- std::f64::consts::PI;
|
||||
let latitude = geo.reference_point.y.to_radians();
|
||||
let east_component = -declination.cos() * hour_angle.sin();
|
||||
let north_component = declination.sin() * latitude.cos()
|
||||
- declination.cos() * hour_angle.cos() * latitude.sin();
|
||||
let up_component = declination.sin() * latitude.sin()
|
||||
+ declination.cos() * hour_angle.cos() * latitude.cos();
|
||||
if up_component <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let north = normalized(
|
||||
[geo.north_direction.x, geo.north_direction.y, 0.0],
|
||||
[0.0, 1.0, 0.0],
|
||||
);
|
||||
let east = [north[1], -north[0], 0.0];
|
||||
let up = normalized(
|
||||
[geo.up_direction.x, geo.up_direction.y, geo.up_direction.z],
|
||||
[0.0, 0.0, 1.0],
|
||||
);
|
||||
Some(normalized(
|
||||
[
|
||||
-(east[0] as f64 * east_component
|
||||
+ north[0] as f64 * north_component
|
||||
+ up[0] as f64 * up_component),
|
||||
-(east[1] as f64 * east_component
|
||||
+ north[1] as f64 * north_component
|
||||
+ up[1] as f64 * up_component),
|
||||
-(east[2] as f64 * east_component
|
||||
+ north[2] as f64 * north_component
|
||||
+ up[2] as f64 * up_component),
|
||||
],
|
||||
[0.0, 0.0, -1.0],
|
||||
))
|
||||
}
|
||||
|
||||
fn converted(scene: &Scene, light: &acadrust::entities::Light) -> Option<SceneLight> {
|
||||
if !light.status {
|
||||
return None;
|
||||
}
|
||||
let direction = normalized(
|
||||
[
|
||||
light.target.x - light.position.x,
|
||||
light.target.y - light.position.y,
|
||||
light.target.z - light.position.z,
|
||||
],
|
||||
[0.0, 0.0, -1.0],
|
||||
);
|
||||
let color_layer = if light.light_color.rgb().is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(light.common.layer.clone())
|
||||
};
|
||||
let rgba = if color_layer.is_none() {
|
||||
tess_util::aci_to_rgba(&light.light_color)
|
||||
} else {
|
||||
scene.layer_color(&light.common.layer)
|
||||
};
|
||||
Some(SceneLight {
|
||||
handle: light.common.handle,
|
||||
color_layer,
|
||||
light_type: light.light_type as f32,
|
||||
position: [light.position.x, light.position.y, light.position.z],
|
||||
direction,
|
||||
color: [rgba[0], rgba[1], rgba[2]],
|
||||
intensity: light.intensity.max(0.0) as f32,
|
||||
hotspot_cos: if light.hotspot_angle > 0.0 {
|
||||
(light.hotspot_angle * 0.5).cos() as f32
|
||||
} else {
|
||||
1.0
|
||||
},
|
||||
falloff_cos: if light.falloff_angle > 0.0 {
|
||||
(light.falloff_angle * 0.5).cos() as f32
|
||||
} else {
|
||||
-1.0
|
||||
},
|
||||
attenuation_type: light.attenuation_type as f32,
|
||||
attenuation_start: if light.use_attenuation_limits {
|
||||
light.attenuation_start_limit as f32
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
attenuation_end: if light.use_attenuation_limits {
|
||||
light.attenuation_end_limit as f32
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let mut lights = Vec::new();
|
||||
for &handle in crate::entities::object_data::light_entities(
|
||||
&self.object_data_cache,
|
||||
) {
|
||||
if lights.len() >= 4 {
|
||||
break;
|
||||
}
|
||||
if let Some(EntityType::Light(light)) = self.document.get_entity(handle) {
|
||||
if let Some(light) = converted(self, light) {
|
||||
lights.push(light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lights.len() < 4 {
|
||||
let geo = crate::entities::object_data::geo_objects(
|
||||
&self.object_data_cache,
|
||||
)
|
||||
.iter()
|
||||
.find_map(|handle| match self.document.objects.get(handle) {
|
||||
Some(ObjectType::GeoData(value))
|
||||
if value.coordinate_type == 3
|
||||
&& value.reference_point.x.is_finite()
|
||||
&& value.reference_point.y.is_finite()
|
||||
&& value.reference_point.x.abs() <= 180.0
|
||||
&& value.reference_point.y.abs() <= 90.0 => Some(value),
|
||||
_ => None,
|
||||
});
|
||||
for handle in crate::entities::object_data::sun_objects(
|
||||
&self.object_data_cache,
|
||||
) {
|
||||
let Some(ObjectType::ClassObject(value)) =
|
||||
self.document.objects.get(handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let ClassObjectData::Sun(sun) = &value.data else {
|
||||
continue;
|
||||
};
|
||||
if !sun.is_on {
|
||||
continue;
|
||||
}
|
||||
let Some(geo) = geo else {
|
||||
break;
|
||||
};
|
||||
let Some(direction) = solar_direction(sun, geo) else {
|
||||
break;
|
||||
};
|
||||
let rgba = tess_util::aci_to_rgba(&sun.color);
|
||||
lights.push(SceneLight {
|
||||
handle: value.handle,
|
||||
color_layer: None,
|
||||
light_type: 1.0,
|
||||
position: [0.0; 3],
|
||||
direction,
|
||||
color: [rgba[0], rgba[1], rgba[2]],
|
||||
intensity: sun.intensity.max(0.0) as f32,
|
||||
hotspot_cos: 1.0,
|
||||
falloff_cos: -1.0,
|
||||
attenuation_type: 0.0,
|
||||
attenuation_start: 0.0,
|
||||
attenuation_end: 0.0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
lights
|
||||
}
|
||||
|
||||
fn apply_document_lighting(&self, uniforms: &mut Uniforms) {
|
||||
if self.lighting_cache.borrow().is_none() {
|
||||
let lights = self.build_lighting_cache();
|
||||
*self.lighting_cache.borrow_mut() = Some(lights);
|
||||
}
|
||||
let cache = self.lighting_cache.borrow();
|
||||
let lights = cache.as_deref().unwrap_or_default();
|
||||
let eye = [
|
||||
uniforms.eye_high[0] as f64 + uniforms.eye_low[0] as f64,
|
||||
uniforms.eye_high[1] as f64 + uniforms.eye_low[1] as f64,
|
||||
uniforms.eye_high[2] as f64 + uniforms.eye_low[2] as f64,
|
||||
];
|
||||
uniforms.lighting[0] = lights.len().min(4) as f32;
|
||||
for (index, light) in lights.iter().take(4).enumerate() {
|
||||
let color = light
|
||||
.color_layer
|
||||
.as_deref()
|
||||
.map(|layer| self.layer_color(layer))
|
||||
.map(|rgba| [rgba[0], rgba[1], rgba[2]])
|
||||
.unwrap_or(light.color);
|
||||
uniforms.light_position_type[index] = [
|
||||
(light.position[0] - eye[0]) as f32,
|
||||
(light.position[1] - eye[1]) as f32,
|
||||
(light.position[2] - eye[2]) as f32,
|
||||
light.light_type,
|
||||
];
|
||||
uniforms.light_direction_intensity[index] = [
|
||||
light.direction[0],
|
||||
light.direction[1],
|
||||
light.direction[2],
|
||||
light.intensity,
|
||||
];
|
||||
uniforms.light_color_hotspot[index] = [
|
||||
color[0],
|
||||
color[1],
|
||||
color[2],
|
||||
light.hotspot_cos,
|
||||
];
|
||||
uniforms.light_attenuation[index] = [
|
||||
light.attenuation_type,
|
||||
light.attenuation_start,
|
||||
light.attenuation_end,
|
||||
light.falloff_cos,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns (entity_color, pattern_length, pattern, line_weight_px, aci).
|
||||
pub(in crate::scene) fn render_style(&self, e: &EntityType) -> ([f32; 4], f32, [f32; 8], f32, u8) {
|
||||
let (color, pl, pat, lw, aci) = render_style_for(&self.document, e);
|
||||
|
|
@ -1695,6 +1949,7 @@ impl Scene {
|
|||
uniforms.viewport_size = [visible_w, visible_h];
|
||||
uniforms.flat_shade = if flags.flat_shade { 1.0 } else { 0.0 };
|
||||
uniforms.transparency_enable = if self.transparency_display { 1.0 } else { 0.0 };
|
||||
self.apply_document_lighting(&mut uniforms);
|
||||
|
||||
// `screen_rect` carries the *visible* sub-rectangle in normalized
|
||||
// canvas coords — that's what `Pipeline::prepare` uses to size
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ struct Uniforms {
|
|||
_pad_eh: f32,
|
||||
eye_low: vec3<f32>,
|
||||
_pad_el: f32,
|
||||
light_position_type: array<vec4<f32>, 4>,
|
||||
light_direction_intensity: array<vec4<f32>, 4>,
|
||||
light_color_hotspot: array<vec4<f32>, 4>,
|
||||
light_attenuation: array<vec4<f32>, 4>,
|
||||
lighting: vec4<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0)
|
||||
|
|
@ -161,18 +166,68 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
|
|||
n = normalize(n - slope * maps.blends1.x * max(in.advanced.y, 0.0) * 4.0);
|
||||
}
|
||||
|
||||
// Three-point-ish lighting (world space) plus ambient. Spread directions
|
||||
// keep every face — and the back faces seen through an open surface — lit
|
||||
// from at least one source, so the model never reads as a flat dark mass.
|
||||
// `abs(dot)` makes each light two-sided (independent of normal direction).
|
||||
// Native AcDbLight / Sun lighting. When a drawing has no active lights,
|
||||
// retain the neutral three-point editor rig so ordinary models remain
|
||||
// readable.
|
||||
let l0 = normalize(vec3<f32>( 0.5, 0.8, 0.6)); // key (upper front)
|
||||
let l1 = normalize(vec3<f32>(-0.7, 0.3, 0.4)); // fill (left)
|
||||
let l2 = normalize(vec3<f32>( 0.2, -0.6, -0.8)); // back/under
|
||||
let diff = 0.45 * abs(dot(n, l0))
|
||||
var direct_light = vec3<f32>(
|
||||
0.45 * abs(dot(n, l0))
|
||||
+ 0.30 * abs(dot(n, l1))
|
||||
+ 0.25 * abs(dot(n, l2));
|
||||
+ 0.25 * abs(dot(n, l2))
|
||||
);
|
||||
var key_light = l0;
|
||||
if (u.lighting.x > 0.5) {
|
||||
direct_light = vec3<f32>(0.0);
|
||||
let count = min(u32(u.lighting.x), 4u);
|
||||
for (var index = 0u; index < count; index = index + 1u) {
|
||||
let position_type = u.light_position_type[index];
|
||||
let direction_intensity = u.light_direction_intensity[index];
|
||||
let color_hotspot = u.light_color_hotspot[index];
|
||||
let attenuation_data = u.light_attenuation[index];
|
||||
var light_vector = -normalize(direction_intensity.xyz);
|
||||
var attenuation = 1.0;
|
||||
if (position_type.w > 1.5) {
|
||||
let delta = position_type.xyz - in.world_pos;
|
||||
let distance = max(length(delta), 1e-5);
|
||||
light_vector = delta / distance;
|
||||
if (attenuation_data.x > 1.5) {
|
||||
attenuation /= max(distance * distance, 1.0);
|
||||
} else if (attenuation_data.x > 0.5) {
|
||||
attenuation /= max(distance, 1.0);
|
||||
}
|
||||
if (attenuation_data.z > attenuation_data.y) {
|
||||
attenuation *= 1.0 - smoothstep(
|
||||
attenuation_data.y,
|
||||
attenuation_data.z,
|
||||
distance,
|
||||
);
|
||||
}
|
||||
if (position_type.w > 2.5) {
|
||||
let source_to_fragment = -light_vector;
|
||||
let cone = dot(
|
||||
source_to_fragment,
|
||||
normalize(direction_intensity.xyz),
|
||||
);
|
||||
attenuation *= smoothstep(
|
||||
attenuation_data.w,
|
||||
color_hotspot.w,
|
||||
cone,
|
||||
);
|
||||
}
|
||||
}
|
||||
let strength = max(direction_intensity.w, 0.0) * attenuation;
|
||||
direct_light += color_hotspot.rgb
|
||||
* max(dot(n, light_vector), 0.0)
|
||||
* strength;
|
||||
if (index == 0u) {
|
||||
key_light = light_vector;
|
||||
}
|
||||
}
|
||||
}
|
||||
let view = normalize(in.eye_vec);
|
||||
let half_vec = normalize(l0 + view);
|
||||
let half_vec = normalize(key_light + view);
|
||||
let gloss_exp = mix(2.0, 128.0, clamp(in.material.x, 0.0, 1.0));
|
||||
let fresnel0 = pow(
|
||||
(max(in.specular.w, 1.0) - 1.0) / (max(in.specular.w, 1.0) + 1.0),
|
||||
|
|
@ -186,8 +241,13 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
|
|||
let texel = textureSample(specular_map, specular_sampler, in.uv_specular).rgb;
|
||||
specular_color = mix(specular_color, texel, clamp(maps.blends0.y, 0.0, 1.0));
|
||||
}
|
||||
let half_response = select(
|
||||
abs(dot(n, half_vec)),
|
||||
max(dot(n, half_vec), 0.0),
|
||||
u.lighting.x > 0.5,
|
||||
);
|
||||
let specular = specular_color
|
||||
* pow(max(abs(dot(n, half_vec)), 0.0), gloss_exp)
|
||||
* pow(half_response, gloss_exp)
|
||||
* specular_strength
|
||||
* max(in.advanced.z, 0.0);
|
||||
var albedo = in.color.rgb;
|
||||
|
|
@ -196,7 +256,7 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
|
|||
albedo = mix(albedo, texel, clamp(maps.blends0.x, 0.0, 1.0));
|
||||
}
|
||||
let ambient_light = albedo * clamp(in.ambient.rgb, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||
var lit = ambient_light + albedo * clamp(diff, 0.0, 1.0) + specular;
|
||||
var lit = ambient_light + albedo * clamp(direct_light, vec3<f32>(0.0), vec3<f32>(2.0)) + specular;
|
||||
if (maps.present0.z != 0u) {
|
||||
let reflected = textureSample(
|
||||
reflection_map,
|
||||
|
|
|
|||
Loading…
Reference in a new issue