Skip to content

Commit

Permalink
Support loading images and meshes on web (rerun-io#3131)
Browse files Browse the repository at this point in the history
* Closes rerun-io#2229

### What
Drag-dropping images and meshes now works on web, as does File->Open.

I also managed to get rid of the use of `MsgSender` to create a
`LogMsg`, simplifying the dependencies a bit (if not the code).

### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested [demo.rerun.io](https://demo.rerun.io/pr/3131) (if
applicable)

- [PR Build Summary](https://build.rerun.io/pr/3131)
- [Docs
preview](https://rerun.io/preview/551ba6c6df4dd6d2d13760f51942e83bff3fe4ce/docs)
<!--DOCS-PREVIEW-->
- [Examples
preview](https://rerun.io/preview/551ba6c6df4dd6d2d13760f51942e83bff3fe4ce/examples)
<!--EXAMPLES-PREVIEW--><!--EXAMPLES-PREVIEW-->
- [Recent benchmark results](https://ref.rerun.io/dev/bench/)
- [Wasm size tracking](https://ref.rerun.io/dev/sizes/)
  • Loading branch information
emilk authored Aug 29, 2023
1 parent 71a5f53 commit 80cc99d
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 63 deletions.
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))
}

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

0 comments on commit 80cc99d

Please sign in to comment.