Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support loading images and meshes on web #3131

Merged
merged 4 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions crates/re_components/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ mod text_box;
mod text_entry;
mod vec;

#[cfg(not(target_arch = "wasm32"))]
mod load_file;

#[cfg(feature = "arrow_datagen")]
Expand Down Expand Up @@ -66,7 +65,9 @@ pub use self::{
pub use self::tensor::{TensorImageLoadError, TensorImageSaveError};

#[cfg(not(target_arch = "wasm32"))]
pub use self::load_file::{data_cell_from_file_path, data_cell_from_mesh_file_path, FromFileError};
pub use self::load_file::{data_cell_from_file_path, data_cell_from_mesh_file_path};

pub use self::load_file::{data_cell_from_file_contents, FromFileError};

// This is very convenient to re-export
pub use re_log_types::LegacyComponent;
Expand Down
52 changes: 51 additions & 1 deletion crates/re_components/src/load_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use re_log_types::DataCell;
/// Errors from [`data_cell_from_file_path`] and [`data_cell_from_mesh_file_path`].
#[derive(thiserror::Error, Debug)]
pub enum FromFileError {
#[cfg(not(target_arch = "wasm32"))]
#[error(transparent)]
FileRead(#[from] std::io::Error),

Expand All @@ -28,6 +29,7 @@ pub enum FromFileError {
/// * `png` and other image formats: decoded here. Requires the `image` feature.
///
/// All other extensions will return an error.
#[cfg(not(target_arch = "wasm32"))]
pub fn data_cell_from_file_path(file_path: &std::path::Path) -> Result<DataCell, FromFileError> {
let extension = file_path
.extension()
Expand Down Expand Up @@ -56,20 +58,68 @@ pub fn data_cell_from_file_path(file_path: &std::path::Path) -> Result<DataCell,
}
}

pub fn data_cell_from_file_contents(
file_name: &str,
bytes: Vec<u8>,
) -> Result<DataCell, FromFileError> {
re_tracing::profile_function!(file_name);

let extension = std::path::Path::new(file_name)
.extension()
.unwrap_or_default()
.to_ascii_lowercase()
.to_string_lossy()
.to_string();

match extension.as_str() {
"glb" => data_cell_from_mesh_file_contents(bytes, crate::MeshFormat::Glb),
"glft" => data_cell_from_mesh_file_contents(bytes, crate::MeshFormat::Gltf),
"obj" => data_cell_from_mesh_file_contents(bytes, crate::MeshFormat::Obj),

#[cfg(feature = "image")]
_ => {
let format = if let Some(format) = image::ImageFormat::from_extension(extension) {
format
} else {
image::guess_format(&bytes).map_err(crate::TensorImageLoadError::from)?
};

// Assume an image (there are so many image extensions):
let tensor = crate::Tensor::from_image_bytes(bytes, format)?;
Ok(DataCell::try_from_native(std::iter::once(&tensor))?)
}

#[cfg(not(feature = "image"))]
_ => Err(FromFileError::UnknownExtension {
extension,
path: file_name.to_owned().into(),
}),
}
}

/// Read the mesh file at the given path.
///
/// Supported file extensions are:
/// * `glb`, `gltf`, `obj`: encoded meshes, leaving it to the viewer to decode
///
/// All other extensions will return an error.
#[cfg(not(target_arch = "wasm32"))]
pub fn data_cell_from_mesh_file_path(
file_path: &std::path::Path,
format: crate::MeshFormat,
) -> Result<DataCell, FromFileError> {
let bytes = std::fs::read(file_path)?;
data_cell_from_mesh_file_contents(bytes, format)
}

pub fn data_cell_from_mesh_file_contents(
bytes: Vec<u8>,
format: crate::MeshFormat,
) -> Result<DataCell, FromFileError> {
let mesh = crate::EncodedMesh3D {
mesh_id: crate::MeshId::random(),
format,
bytes: std::fs::read(file_path)?.into(),
bytes: bytes.into(),
transform: [
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
Expand Down
6 changes: 1 addition & 5 deletions crates/re_data_source/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ all-features = true
[features]
default = []

sdk = ["dep:re_sdk"]


[dependencies]
re_components.workspace = true
re_log_encoding = { workspace = true, features = ["decoder"] }
re_log_types.workspace = true
re_log.workspace = true
Expand All @@ -34,9 +33,6 @@ anyhow.workspace = true
itertools.workspace = true
rayon.workspace = true

# Optional:
re_sdk = { workspace = true, optional = true }


[build-dependencies]
re_build_tools.workspace = true
6 changes: 6 additions & 0 deletions crates/re_data_source/src/data_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ impl DataSource {
let store_id = re_log_types::StoreId::random(re_log_types::StoreKind::Recording);
crate::load_file_path::load_file_path(store_id, path.clone(), tx)
.with_context(|| format!("{path:?}"))?;
if let Some(on_msg) = on_msg {
on_msg();
}
Ok(rx)
}

Expand All @@ -129,6 +132,9 @@ impl DataSource {
let store_id = re_log_types::StoreId::random(re_log_types::StoreKind::Recording);
crate::load_file_contents::load_file_contents(store_id, file_contents, tx)
.with_context(|| format!("{name:?}"))?;
if let Some(on_msg) = on_msg {
on_msg();
}
Ok(rx)
}

Expand Down
77 changes: 74 additions & 3 deletions crates/re_data_source/src/load_file_contents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::FileContents;

#[allow(clippy::needless_pass_by_value)] // false positive on some feature flags
pub fn load_file_contents(
_store_id: re_log_types::StoreId,
store_id: re_log_types::StoreId,
file_contents: FileContents,
tx: Sender<LogMsg>,
) -> anyhow::Result<()> {
Expand All @@ -26,11 +26,82 @@ pub fn load_file_contents(
Ok(())
}
} else {
// TODO(emilk): support loading images and meshes from file contents
anyhow::bail!("Unsupported file extension for {file_name:?}.");
// non-rrd = image or mesh:
if cfg!(target_arch = "wasm32") {
load_and_send(store_id, file_contents, &tx)
} else {
rayon::spawn(move || {
let name = file_contents.name.clone();
if let Err(err) = load_and_send(store_id, file_contents, &tx) {
re_log::error!("Failed to load {name:?}: {err}");
}
});
Ok(())
}
}
}

fn load_and_send(
store_id: re_log_types::StoreId,
file_contents: FileContents,
tx: &Sender<LogMsg>,
) -> anyhow::Result<()> {
use re_log_types::SetStoreInfo;

re_tracing::profile_function!(file_contents.name.as_str());

// First, set a store info since this is the first thing the application expects.
tx.send(LogMsg::SetStoreInfo(SetStoreInfo {
row_id: re_log_types::RowId::random(),
info: re_log_types::StoreInfo {
application_id: re_log_types::ApplicationId(file_contents.name.clone()),
store_id: store_id.clone(),
is_official_example: false,
started: re_log_types::Time::now(),
store_source: re_log_types::StoreSource::FileFromCli {
rustc_version: env!("RE_BUILD_RUSTC_VERSION").into(),
llvm_version: env!("RE_BUILD_LLVM_VERSION").into(),
},
store_kind: re_log_types::StoreKind::Recording,
},
}))
.ok();
// .ok(): we may be running in a background thread, so who knows if the receiver is still open

// Send actual file.
let log_msg = log_msg_from_file_contents(store_id, file_contents)?;
tx.send(log_msg).ok();
tx.quit(None).ok();
Ok(())
}

fn log_msg_from_file_contents(
store_id: re_log_types::StoreId,
file_contents: FileContents,
) -> anyhow::Result<LogMsg> {
let FileContents { name, bytes } = file_contents;

let entity_path = re_log_types::EntityPath::from_single_string(name.clone());
let cell = re_components::data_cell_from_file_contents(&name, bytes.to_vec())?;

let num_instances = cell.num_instances();

let timepoint = re_log_types::TimePoint::default();

let data_row = re_log_types::DataRow::from_cells(
re_log_types::RowId::random(),
timepoint,
entity_path,
num_instances,
vec![cell],
);

let data_table =
re_log_types::DataTable::from_rows(re_log_types::TableId::random(), [data_row]);
let arrow_msg = data_table.to_arrow_msg()?;
Ok(LogMsg::ArrowMsg(store_id, arrow_msg))
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating log messages is quite verbose 😬

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least it doesn't look too arcane.


fn load_rrd_sync(file_contents: &FileContents, tx: &Sender<LogMsg>) -> Result<(), anyhow::Error> {
re_tracing::profile_function!(file_contents.name.as_str());

Expand Down
110 changes: 64 additions & 46 deletions crates/re_data_source/src/load_file_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,54 +27,72 @@ pub fn load_file_path(
if extension == "rrd" {
stream_rrd_file(path, tx)
} else {
#[cfg(feature = "sdk")]
{
rayon::spawn(move || {
use re_log_types::SetStoreInfo;
// First, set a store info since this is the first thing the application expects.
tx.send(LogMsg::SetStoreInfo(SetStoreInfo {
row_id: re_log_types::RowId::random(),
info: re_log_types::StoreInfo {
application_id: re_log_types::ApplicationId(path.display().to_string()),
store_id: store_id.clone(),
is_official_example: false,
started: re_log_types::Time::now(),
store_source: re_log_types::StoreSource::FileFromCli {
rustc_version: env!("RE_BUILD_RUSTC_VERSION").into(),
llvm_version: env!("RE_BUILD_LLVM_VERSION").into(),
},
store_kind: re_log_types::StoreKind::Recording,
},
}))
.ok(); // .ok(): we may be running in a background thread, so who knows if the receiver is still open

// Send actual file.
match re_sdk::MsgSender::from_file_path(&path) {
Ok(msg_sender) => match msg_sender.into_log_msg(store_id) {
Ok(log_msg) => {
tx.send(log_msg).ok();
}

Err(err) => {
re_log::error!("Failed to load {path:?}: {err}");
}
},
Err(err) => {
re_log::error!("Failed to load {path:?}: {err}");
}
}
rayon::spawn(move || {
if let Err(err) = load_and_send(store_id, &path, &tx) {
re_log::error!("Failed to load {path:?}: {err}");
}
});
Ok(())
}
}

tx.quit(None).ok();
});
Ok(())
}
fn load_and_send(
store_id: re_log_types::StoreId,
path: &std::path::Path,
tx: &Sender<LogMsg>,
) -> anyhow::Result<()> {
re_tracing::profile_function!(path.display().to_string());

#[cfg(not(feature = "sdk"))]
{
_ = store_id;
anyhow::bail!("Unsupported file extension: '{extension}' for path {path:?}. Try enabling the 'sdk' feature of 'rerun'.");
}
}
use re_log_types::SetStoreInfo;

// First, set a store info since this is the first thing the application expects.
tx.send(LogMsg::SetStoreInfo(SetStoreInfo {
row_id: re_log_types::RowId::random(),
info: re_log_types::StoreInfo {
application_id: re_log_types::ApplicationId(path.display().to_string()),
store_id: store_id.clone(),
is_official_example: false,
started: re_log_types::Time::now(),
store_source: re_log_types::StoreSource::FileFromCli {
rustc_version: env!("RE_BUILD_RUSTC_VERSION").into(),
llvm_version: env!("RE_BUILD_LLVM_VERSION").into(),
},
store_kind: re_log_types::StoreKind::Recording,
},
}))
.ok();
// .ok(): we may be running in a background thread, so who knows if the receiver is still open

// Send actual file.
let log_msg = log_msg_from_file_path(store_id, path)?;
tx.send(log_msg).ok();
tx.quit(None).ok();
Ok(())
}

fn log_msg_from_file_path(
store_id: re_log_types::StoreId,
file_path: &std::path::Path,
) -> anyhow::Result<LogMsg> {
let entity_path = re_log_types::EntityPath::from_file_path_as_single_string(file_path);
let cell = re_components::data_cell_from_file_path(file_path)?;

let num_instances = cell.num_instances();

let timepoint = re_log_types::TimePoint::default();

let data_row = re_log_types::DataRow::from_cells(
re_log_types::RowId::random(),
timepoint,
entity_path,
num_instances,
vec![cell],
);

let data_table =
re_log_types::DataTable::from_rows(re_log_types::TableId::random(), [data_row]);
let arrow_msg = data_table.to_arrow_msg()?;
Ok(LogMsg::ArrowMsg(store_id, arrow_msg))
}

// Non-blocking
Expand Down
9 changes: 6 additions & 3 deletions crates/re_log_types/src/path/entity_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,12 @@ impl EntityPath {
/// The returned path will only have one part.
#[cfg(not(target_arch = "wasm32"))]
pub fn from_file_path_as_single_string(file_path: &std::path::Path) -> Self {
Self::new(vec![EntityPathPart::Index(crate::Index::String(
file_path.to_string_lossy().to_string(),
))])
Self::from_single_string(file_path.to_string_lossy().to_string())
}

/// Treat the string as one opaque string, NOT splitting on any slashes.
pub fn from_single_string(string: String) -> Self {
Self::new(vec![EntityPathPart::Index(crate::Index::String(string))])
}

#[inline]
Expand Down
Loading