fix(scene): preserve viewport settings

Reuse full VPort records during camera and tile synchronization so
navigation cannot replace file-defined display state with defaults.

Honor image, ground, IBL, and sun-backed sky parameters in
background rendering.
This commit is contained in:
Hakan Seven 2026-08-12 13:10:55 +03:00
commit 15ab027d70
3 changed files with 357 additions and 179 deletions

View file

@ -34,6 +34,7 @@ struct ViewportLightingSettings {
force_default: bool,
default_type: i16,
ambient: [f32; 3],
sun_handle: Handle,
}
#[derive(Clone)]
@ -94,6 +95,7 @@ impl Default for ViewportLightingSettings {
force_default: true,
default_type: 1,
ambient: [0.18; 3],
sun_handle: Handle::NULL,
}
}
}
@ -1426,9 +1428,148 @@ fn crop_view_proj(view_proj: glam::Mat4, uo: f32, vo: f32, us: f32, vs: f32) ->
crop * view_proj
}
fn normalized_direction(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_direction(
[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_direction(
[geo.up_direction.x, geo.up_direction.y, geo.up_direction.z],
[0.0, 0.0, 1.0],
);
Some(normalized_direction(
[
-(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],
))
}
// ── Render-style helpers (impl Scene) ────────────────────────────────────
impl Scene {
fn model_tile_vport(&self, index: usize) -> Option<&acadrust::tables::VPort> {
let rect = self.model_tiles.borrow().get(index)?.rect;
let lower_left = [rect.x as f64, (1.0 - rect.y - rect.height) as f64];
let upper_right = [
(rect.x + rect.width) as f64,
(1.0 - rect.y) as f64,
];
const EPSILON: f64 = 1e-5;
let exact = self
.document
.vports
.iter()
.filter(|value| value.name.eq_ignore_ascii_case("*Active"))
.find(|value| {
(value.lower_left.x - lower_left[0]).abs() <= EPSILON
&& (value.lower_left.y - lower_left[1]).abs() <= EPSILON
&& (value.upper_right.x - upper_right[0]).abs() <= EPSILON
&& (value.upper_right.y - upper_right[1]).abs() <= EPSILON
});
exact.or_else(|| {
let center_x = (lower_left[0] + upper_right[0]) * 0.5;
let center_y = (lower_left[1] + upper_right[1]) * 0.5;
self.document
.vports
.iter()
.filter(|value| {
value.name.eq_ignore_ascii_case("*Active")
&& center_x >= value.lower_left.x - EPSILON
&& center_x <= value.upper_right.x + EPSILON
&& center_y >= value.lower_left.y - EPSILON
&& center_y <= value.upper_right.y + EPSILON
})
.min_by(|left, right| {
let left_area = (left.upper_right.x - left.lower_left.x)
* (left.upper_right.y - left.lower_left.y);
let right_area = (right.upper_right.x - right.lower_left.x)
* (right.upper_right.y - right.lower_left.y);
left_area.total_cmp(&right_area)
})
.or_else(|| {
self.document
.vports
.iter()
.find(|value| value.name.eq_ignore_ascii_case("*Active"))
})
})
}
fn geolocation(&self) -> Option<&acadrust::objects::GeoData> {
use acadrust::objects::ObjectType;
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,
})
}
fn build_lighting_cache(
&self,
target_block: Handle,
@ -1436,92 +1577,11 @@ impl Scene {
) -> 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 mut direction = normalized(
let mut direction = normalized_direction(
[
light.target.x - light.position.x,
light.target.y - light.position.y,
@ -1687,19 +1747,7 @@ impl Scene {
}
}
// SUN is document-global. Keep it after entity lights so the four-light
// shader limit prefers visible lights owned by this viewport's block.
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,
});
let geo = self.geolocation();
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;
@ -1711,10 +1759,10 @@ impl Scene {
continue;
}
let Some(geo) = geo else {
break;
continue;
};
let Some(direction) = solar_direction(sun, geo) else {
break;
continue;
};
let rgba = tess_util::aci_to_rgba(&sun.color);
lights.push(SceneLight {
@ -1737,7 +1785,6 @@ impl Scene {
web_rotation: [0.0; 3],
web_enabled: false,
});
break;
}
lights
}
@ -1756,6 +1803,7 @@ impl Scene {
}
let cache = self.lighting_cache.borrow();
let lights = cache.get(&key).map(Vec::as_slice).unwrap_or_default();
let settings = self.viewport_lighting_settings(viewport);
let visible_lights: Vec<&SceneLight> = lights
.iter()
.filter(|light| match self.document.get_entity(light.handle) {
@ -1777,12 +1825,12 @@ impl Scene {
object,
acadrust::objects::ObjectType::ClassObject(value)
if matches!(&value.data, acadrust::objects::ClassObjectData::Sun(_))
)
) && (!settings.sun_handle.is_valid()
|| settings.sun_handle == light.handle)
}),
})
.take(4)
.collect();
let settings = self.viewport_lighting_settings(viewport);
uniforms.lighting[1..4].copy_from_slice(&settings.ambient);
if settings.force_default || visible_lights.is_empty() {
Self::apply_default_lighting(uniforms, &viewport.camera, settings.default_type);
@ -1933,11 +1981,13 @@ impl Scene {
force_default: value.default_lighting,
default_type: value.default_lighting_type,
ambient: ambient(&value.ambient_color),
sun_handle: value.sun_handle,
};
let from_table = |value: &acadrust::tables::VPort| ViewportLightingSettings {
force_default: value.use_default_lights,
default_type: value.default_lighting_type,
ambient: ambient(&value.ambient_color),
sun_handle: value.sun_handle,
};
if viewport.paper_sheet {
@ -1951,13 +2001,7 @@ impl Scene {
}
} else if self.current_layout == "Model" {
let index = viewport.tile_idx.unwrap_or(0);
if let Some(value) = self
.document
.vports
.iter()
.filter(|value| value.name.eq_ignore_ascii_case("*Active"))
.nth(index)
{
if let Some(value) = self.model_tile_vport(index) {
return from_table(value);
}
}
@ -2025,13 +2069,7 @@ impl Scene {
}
} else if self.current_layout == "Model" {
let index = viewport.tile_idx.unwrap_or(0);
if let Some(value) = self
.document
.vports
.iter()
.filter(|value| value.name.eq_ignore_ascii_case("*Active"))
.nth(index)
{
if let Some(value) = self.model_tile_vport(index) {
return build(
value.visual_style_handle,
value.brightness,
@ -2079,7 +2117,16 @@ impl Scene {
candidates.push(base.join(&source));
if let Some(name) = source.file_name() {
candidates.push(base.join(name));
for folder in ["Textures", "textures", "Materials", "materials"] {
for folder in [
"Textures",
"textures",
"Materials",
"materials",
"Environments",
"environments",
"Render",
"render",
] {
candidates.push(base.join(folder).join(name));
}
}
@ -2373,12 +2420,38 @@ impl Scene {
}
result
}
ClassObjectData::SkyLightBackground(_) => {
ClassObjectData::SkyLightBackground(background) => {
let mut result = ViewportBackgroundSettings::canvas(canvas);
result.colors[0] = [0.18, 0.42, 0.78, 1.0];
result.colors[1] = [1.0, 0.92, 0.72, 0.0];
result.colors[2] = [0.78, 0.86, 0.95, 1.0];
result.base = result.colors[2];
result.params[0] = 6.0;
if let Some(ObjectType::ClassObject(value)) =
self.document.objects.get(&background.sun)
{
if let ClassObjectData::Sun(sun) = &value.data {
if sun.is_on {
if let Some(direction) = self
.geolocation()
.and_then(|geo| solar_direction(sun, geo))
{
result.params[1..4].copy_from_slice(&[
-direction[0],
-direction[1],
-direction[2],
]);
let color = tess_util::aci_to_rgba(&sun.color);
result.colors[1] = [
color[0],
color[1],
color[2],
sun.intensity.max(0.0) as f32,
];
}
}
}
}
result
}
_ => ViewportBackgroundSettings::canvas(canvas),
@ -3467,7 +3540,13 @@ impl Scene {
uniforms.background_image_params = display.background.image_params;
uniforms.background_image_transform = display.background.image_transform;
uniforms.environment_params = display.background.environment_params;
uniforms.environment_view = glam::Mat4::from_quat(inst.camera.rotation);
let environment_scale = (inst.camera.fov_y * 0.5).tan().max(1e-4);
uniforms.environment_view = glam::Mat4::from_quat(inst.camera.rotation)
* glam::Mat4::from_scale(glam::Vec3::new(
visible_w / visible_h.max(1.0) * environment_scale,
environment_scale,
1.0,
));
uniforms.fog_color = display.background.fog_color;
uniforms.fog_params = display.background.fog_params;
uniforms.fog_distances = display.background.fog_distances;