diff --git a/README.md b/README.md
index 6ffe791..2c71f5f 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,8 @@ The property request uses `READ_ACCESS` and omits `propertyName`, which asks Unr
Details payloads are cached in browser `localStorage` for five minutes per object path. When you reselect an object, cached properties render immediately and the app refreshes them from Unreal in the background.
+Asset-looking property values also request `/remote/object/thumbnail` and cache thumbnail data in `localStorage`, so mesh/material fields can render closer to Unreal's Details panel.
+
## Run
Start Unreal Editor, enable the Remote Control API, and make sure it is listening on `127.0.0.1:30010`.
diff --git a/src/components/DetailsPanel.tsx b/src/components/DetailsPanel.tsx
index d72b530..4ca076d 100644
--- a/src/components/DetailsPanel.tsx
+++ b/src/components/DetailsPanel.tsx
@@ -1,8 +1,17 @@
import React from "react";
-import { ChevronDown, Search, Settings, SlidersHorizontal, X } from "lucide-react";
+import { Box, ChevronDown, Grid2X2, RotateCcw, Search, Settings, SlidersHorizontal, Star, X } from "lucide-react";
import type { DetailProperty, ObjectDetails, TreeNode } from "../types";
import { basename } from "../lib/remoteValues";
-import { formatDetailValue } from "../services/details";
+import {
+ detailAssetPath,
+ formatDetailValue,
+ isBooleanProperty,
+ isVectorLikeProperty,
+ normalizeThumbnailPayload,
+ vectorParts,
+} from "../services/details";
+import { readRemoteObjectThumbnail } from "../services/remoteControl";
+import { readCachedThumbnail, writeCachedThumbnail } from "../services/thumbnailCache";
import { NodeIcon } from "./NodeIcon";
const emptyNode: TreeNode = {
@@ -15,6 +24,80 @@ const emptyNode: TreeNode = {
children: [],
};
+const categoryTabs = ["General", "Actor", "LOD", "Misc", "Physics", "Rendering", "Streaming", "All"];
+
+function DetailValue({ property }: { property: DetailProperty }) {
+ const assetPath = detailAssetPath(property);
+ const [thumbnail, setThumbnail] = React.useState(() => (assetPath ? readCachedThumbnail(assetPath) : ""));
+
+ React.useEffect(() => {
+ let isCancelled = false;
+
+ if (!assetPath) {
+ setThumbnail("");
+ return;
+ }
+
+ const cached = readCachedThumbnail(assetPath);
+ if (cached) {
+ setThumbnail(cached);
+ return;
+ }
+
+ readRemoteObjectThumbnail(assetPath)
+ .then((payload) => {
+ const dataUrl = normalizeThumbnailPayload(payload);
+ if (!isCancelled && dataUrl) {
+ setThumbnail(dataUrl);
+ writeCachedThumbnail(assetPath, dataUrl);
+ }
+ })
+ .catch(() => {
+ if (!isCancelled) {
+ setThumbnail("");
+ }
+ });
+
+ return () => {
+ isCancelled = true;
+ };
+ }, [assetPath]);
+
+ if (assetPath) {
+ return (
+
+
{thumbnail ?

:
}
+
+ {formatDetailValue(property.value)}
+
+
+
+ );
+ }
+
+ if (isVectorLikeProperty(property)) {
+ return (
+
+ {vectorParts(property.value).map((part, index) => (
+
+ {part.value}
+
+ ))}
+
+ );
+ }
+
+ if (isBooleanProperty(property)) {
+ return ;
+ }
+
+ return (
+
+ {formatDetailValue(property.value)}
+
+ );
+}
+
export function DetailsPanel({
node,
details,
@@ -27,10 +110,13 @@ export function DetailsPanel({
error: string | null;
}) {
const [query, setQuery] = React.useState("");
+ const [activeCategory, setActiveCategory] = React.useState("All");
const groupedProperties = React.useMemo(() => {
const lowerQuery = query.toLowerCase();
const visibleProperties = (details?.properties ?? []).filter((property) => {
+ const matchesCategory = activeCategory === "All" || property.category.toLowerCase().includes(activeCategory.toLowerCase());
+ if (!matchesCategory) return false;
if (!lowerQuery) return true;
return [property.displayName, property.name, property.type, property.category, formatDetailValue(property.value)].some((field) =>
field.toLowerCase().includes(lowerQuery),
@@ -43,7 +129,7 @@ export function DetailsPanel({
groups[category].push(property);
return groups;
}, {});
- }, [details, query]);
+ }, [activeCategory, details, query]);
return (