use super::{
super::fcstd_container::{DocumentArchive, MAX_XML},
document::{self, ObjectDocument},
};
use crate::Result;
use std::collections::BTreeSet;
const MAX_COLORS: usize = 4096;
const MAX_BYTES: usize = 8 * 1024 * 1024;
const MAX_STRING: usize = 1024 * 1024;
pub(super) fn apply(archive: &mut DocumentArchive, objects: &mut ObjectDocument) -> Result<()> {
let Some(bytes) = archive.read_optional("GuiDocument.xml", MAX_XML)? else {
return Ok(());
};
let text = std::str::from_utf8(&bytes)?;
let xml = super::super::fcstd_container::xml(text)?;
let root = xml.root_element();
if root.tag_name().name() != "Document"
|| root.tag_name().namespace().is_some()
|| root.attribute("SchemaVersion") != Some("1")
{
return Err("FCStd invalid GuiDocument root".into());
}
let Some(providers) = document::child(root, "ViewProviderData")? else {
return Ok(());
};
let mut seen = BTreeSet::new();
for provider in providers
.children()
.filter(|node| node.has_tag_name("ViewProvider"))
{
let name = document::attribute(provider, "name")?;
let Some(&index) = objects.names.get(name) else {
continue;
};
if !seen.insert(index) {
return Err("FCStd duplicate GUI view provider".into());
}
let properties = document::required_child(provider, "Properties")?;
apply_provider(
archive,
&mut objects.objects[index],
properties,
objects.legacy_alpha,
)?;
}
Ok(())
}
fn apply_provider(
archive: &mut DocumentArchive,
object: &mut document::Object,
properties: roxmltree::Node<'_, '_>,
legacy_alpha: Option<bool>,
) -> Result<()> {
let mut properties_seen = BTreeSet::new();
let mut color_file = None;
let mut material_file = None;
for property in properties
.children()
.filter(|node| node.has_tag_name("Property"))
{
let name = document::attribute(property, "name")?;
if !properties_seen.insert(name) {
return Err("FCStd duplicate GUI property".into());
}
match name {
"Visibility" => {
document::typed(property, "App::PropertyBool")?;
let visible = document::boolean(document::required_child(property, "Bool")?)?;
if object.visible.is_none() {
object.visible = Some(visible);
}
}
"ShapeColor" => {
document::typed(property, "App::PropertyColor")?;
let value: u32 = document::attribute(
document::required_child(property, "PropertyColor")?,
"value",
)?
.parse()?;
object.style.color = rgb(value);
object.authored.color = true;
}
"Transparency" => {
document::typed(property, "App::PropertyPercent")?;
let value: u32 =
document::attribute(document::required_child(property, "Integer")?, "value")?
.parse()?;
if value > 100 {
return Err("FCStd transparency must be between 0 and 100".into());
}
object.style.alpha = u8::try_from(255 * (100 - value) / 100)?;
object.authored.opacity = true;
}
"DisplayMode" => {
document::typed(property, "App::PropertyEnumeration")?;
let mode: u32 =
document::attribute(document::required_child(property, "Integer")?, "value")?
.parse()?;
object.style.wireframe = mode == 1 && object.kind.starts_with("Part::Part2DObject");
}
"DiffuseColor" => {
document::typed(property, "App::PropertyColorList")?;
color_file = Some(document::attribute(
document::required_child(property, "ColorList")?,
"file",
)?);
}
"ShapeAppearance" => {
document::typed(property, "App::PropertyMaterialList")?;
let list = document::required_child(property, "MaterialList")?;
let version = list.attribute("version").unwrap_or("0");
if version != "3" {
return Err("FCStd material list requires qualified version 3".into());
}
material_file = Some(document::attribute(list, "file")?);
}
_ => {}
}
}
if let Some(file) = color_file.filter(|file| !file.is_empty()) {
let colors = color_list(&archive.read(file, MAX_BYTES)?, legacy_alpha)?;
let authored = !colors.is_empty();
set_colors(&mut object.style, colors);
object.authored.color |= authored;
}
if let Some(file) = material_file.filter(|file| !file.is_empty()) {
let colors = material_list(&archive.read(file, MAX_BYTES)?)?;
let authored = !colors.is_empty();
set_colors(&mut object.style, colors);
object.authored.color |= authored;
object.authored.opacity |= authored;
}
Ok(())
}
fn set_colors(style: &mut super::super::cad::ShapeStyle, colors: Vec<[u8; 4]>) {
if colors.is_empty() {
return;
}
if colors.len() == 1 {
let [r, g, b, alpha] = colors[0];
style.color = [r, g, b];
style.alpha = style.alpha.min(alpha);
} else {
style.faces = colors;
}
}
fn rgb(value: u32) -> [u8; 3] {
let [r, g, b, _] = value.to_be_bytes();
[r, g, b]
}
fn word(bytes: &[u8], offset: usize) -> Result<u32> {
Ok(u32::from_le_bytes(
bytes
.get(offset..)
.and_then(|tail| tail.get(..4))
.ok_or("FCStd truncated appearance scalar")?
.try_into()?,
))
}
fn count(bytes: &[u8], stride: usize) -> Result<(usize, usize)> {
let count = usize::try_from(word(bytes, 0)?)?;
if count > MAX_COLORS {
return Err("FCStd appearance exceeds 4096 face records".into());
}
let end = count
.checked_mul(stride)
.and_then(|value| value.checked_add(4))
.filter(|&end| end <= bytes.len())
.ok_or("FCStd truncated appearance list")?;
Ok((count, end))
}
fn color_list(bytes: &[u8], legacy: Option<bool>) -> Result<Vec<[u8; 4]>> {
let (count, end) = count(bytes, 4)?;
if end != bytes.len() {
return Err("FCStd color list has trailing bytes".into());
}
let mut colors = Vec::with_capacity(count);
for index in 0..count {
let value = word(bytes, 4 + index * 4)?;
let [r, g, b] = rgb(value);
let packed = value.to_be_bytes()[3];
let alpha = if count == 1 {
255
} else if legacy.ok_or("FCStd per-face opacity requires a known ProgramVersion")? {
255 - packed
} else {
packed
};
colors.push([r, g, b, alpha]);
}
Ok(colors)
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "Finite zero-to-one material opacity is explicitly rounded into a bounded byte."
)]
fn material_list(bytes: &[u8]) -> Result<Vec<[u8; 4]>> {
let (count, mut end) = count(bytes, 24)?;
let mut colors = Vec::with_capacity(count);
for index in 0..count {
let offset = 4 + index * 24;
let [r, g, b] = rgb(word(bytes, offset + 4)?);
let shininess = f32::from_bits(word(bytes, offset + 16)?);
let transparency = f32::from_bits(word(bytes, offset + 20)?);
if !shininess.is_finite()
|| !transparency.is_finite()
|| !(0.0..=1.0).contains(&transparency)
{
return Err("FCStd material has invalid finite opacity".into());
}
let alpha = (255.0_f32 * (1.0 - transparency) + 0.5) as u8;
colors.push([r, g, b, alpha]);
}
for _ in 0..count {
for _ in 0..3 {
let length = usize::try_from(word(bytes, end)?)?;
if length > MAX_STRING {
return Err("FCStd material string exceeds one MiB".into());
}
end = end
.checked_add(4)
.and_then(|value| value.checked_add(length))
.filter(|&end| end <= bytes.len())
.ok_or("FCStd truncated material string")?;
}
}
if end != bytes.len() {
return Err("FCStd material list has trailing bytes".into());
}
Ok(colors)
}