Skip to content
Open
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
176 changes: 90 additions & 86 deletions editor/src/editor/layout/assets-browser/items/material-item.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,90 @@
import { ipcRenderer } from "electron";
import { basename, join, dirname } from "path/posix";
import { pathExists, readJSON, writeJSON } from "fs-extra";

import { ReactNode } from "react";

import { FaRegClone } from "react-icons/fa6";
import { GiMaterialsScience } from "react-icons/gi";

import { Tools } from "babylonjs";

import { UniqueNumber } from "../../../../tools/tools";

import { showAlert, showPrompt } from "../../../../ui/dialog";
import { ContextMenuItem } from "../../../../ui/shadcn/ui/context-menu";

import { openMaterialViewer } from "../viewers/material-viewer";

import { AssetsBrowserItem } from "./item";

export class AssetBrowserMaterialItem extends AssetsBrowserItem {
/**
* @override
*/
protected getIcon(): ReactNode {
return <GiMaterialsScience size="64px" />;
}

/**
* @override
*/
protected async onDoubleClick(): Promise<void> {
const data = await readJSON(this.props.absolutePath);
if (data.customType === "BABYLON.NodeMaterial") {
ipcRenderer.send("window:open", "build/src/editor/windows/nme", {
filePath: this.props.absolutePath,
});
} else {
openMaterialViewer(this.props.editor, this.props.absolutePath);
}
}

/**
* Returns the context menu content for the current item.
* To be overriden by the specialized items implementations.
*/
protected getContextMenuContent(): ReactNode {
return (
<>
<ContextMenuItem className="flex items-center gap-2" onClick={() => this._handleClone()}>
<FaRegClone className="w-4 h-4" /> Clone...
</ContextMenuItem>
</>
);
}

private async _handleClone(): Promise<unknown> {
const data = await readJSON(this.props.absolutePath);
data.id = Tools.RandomId();
data.uniqueId = UniqueNumber.Get();

let name = await showPrompt("Enter the name for the cloned material", undefined, basename(this.props.absolutePath).replace(".material", ""));

if (!name) {
return;
}

if (!name.endsWith(".material")) {
name += ".material";
}

const absoluteDestination = join(dirname(this.props.absolutePath), name);

if (await pathExists(absoluteDestination)) {
return showAlert("Can't clone material", `A material with name ("${name}") already exists in the current folder.`);
}

await writeJSON(absoluteDestination, data, {
spaces: "\t",
encoding: "utf-8",
});

this.props.editor.layout.assets.refresh();
this.props.editor.layout.assets.setSelectedFile(absoluteDestination);
}
}
import { ipcRenderer } from "electron";
import { basename, join, dirname } from "path/posix";
import { pathExists, readJSON, writeJSON } from "fs-extra";

import { ReactNode } from "react";

import { FaRegClone } from "react-icons/fa6";

import { Tools } from "babylonjs";

import { UniqueNumber } from "../../../../tools/tools";

import { showAlert, showPrompt } from "../../../../ui/dialog";
import { ContextMenuItem } from "../../../../ui/shadcn/ui/context-menu";

import { openMaterialViewer } from "../viewers/material-viewer";
import { MaterialThumbnailRenderer } from "../renderers/material-thumbnail";

import { AssetsBrowserItem } from "./item";

export class AssetBrowserMaterialItem extends AssetsBrowserItem {
/**
* @override
*/
protected getIcon(): ReactNode {
return (
<div className="w-full h-full pointer-events-none">
<MaterialThumbnailRenderer absolutePath={this.props.absolutePath} />
</div>
);
}

/**
* @override
*/
protected async onDoubleClick(): Promise<void> {
const data = await readJSON(this.props.absolutePath);
if (data.customType === "BABYLON.NodeMaterial") {
ipcRenderer.send("window:open", "build/src/editor/windows/nme", {
filePath: this.props.absolutePath,
});
} else {
openMaterialViewer(this.props.editor, this.props.absolutePath);
}
}

/**
* Returns the context menu content for the current item.
* To be overriden by the specialized items implementations.
*/
protected getContextMenuContent(): ReactNode {
return (
<>
<ContextMenuItem className="flex items-center gap-2" onClick={() => this._handleClone()}>
<FaRegClone className="w-4 h-4" /> Clone...
</ContextMenuItem>
</>
);
}

private async _handleClone(): Promise<unknown> {
const data = await readJSON(this.props.absolutePath);
data.id = Tools.RandomId();
data.uniqueId = UniqueNumber.Get();

let name = await showPrompt("Enter the name for the cloned material", undefined, basename(this.props.absolutePath).replace(".material", ""));

if (!name) {
return;
}

if (!name.endsWith(".material")) {
name += ".material";
}

const absoluteDestination = join(dirname(this.props.absolutePath), name);

if (await pathExists(absoluteDestination)) {
return showAlert("Can't clone material", `A material with name ("${name}") already exists in the current folder.`);
}

await writeJSON(absoluteDestination, data, {
spaces: "\t",
encoding: "utf-8",
});

this.props.editor.layout.assets.refresh();
this.props.editor.layout.assets.setSelectedFile(absoluteDestination);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { useEffect, useRef, useState } from "react";
import { Engine, Scene, CreateSphere, Vector3, DirectionalLight, HemisphericLight, UniversalCamera, Material, CubeTexture, Color3 } from "babylonjs";

import { GiMaterialsScience } from "react-icons/gi";

import { projectConfiguration } from "../../../../project/configuration";

export interface IMaterialThumbnailRendererProps {
/**
* The absolute path to the material file.
*/
absolutePath: string;
/**
* The width of the thumbnail.
*/
width?: number;
/**
* The height of the thumbnail.
*/
height?: number;
/**
* Optional environment texture to use.
*/
environmentTexture?: CubeTexture;
}

export function MaterialThumbnailRenderer(props: IMaterialThumbnailRendererProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const { width = 120, height = 120 } = props;
const [loadError, setLoadError] = useState(false);

useEffect(() => {
if (!projectConfiguration.path || !canvasRef.current) {
return;
}

// Create engine with appropriate settings for thumbnails
const engine = new Engine(canvasRef.current, true, {
antialias: true,
audioEngine: false,
adaptToDeviceRatio: true,
preserveDrawingBuffer: true,
premultipliedAlpha: false,
});

const scene = new Scene(engine);
scene.clearColor.set(0, 0, 0, 0);

const camera = new UniversalCamera("UniversalCamera", new Vector3(0, 10, 30), scene);
camera.fov = 0.4;
camera.minZ = 0.1;

const dirLight = new DirectionalLight("dirLight", new Vector3(4, -4, -4), scene);
dirLight.intensity = 1;
dirLight.position = new Vector3(100, 100, 100);
dirLight.diffuse = new Color3(1, 1, 1);
dirLight.specular = new Color3(1, 1, 1);

const hemiLight = new HemisphericLight("hemiLight", new Vector3(0, 1, 0), scene);
hemiLight.intensity = 0.2;
hemiLight.diffuse = new Color3(1, 1, 1);
hemiLight.groundColor = new Color3(1, 1, 1);

const sphere = CreateSphere("sphere", { diameter: 10, segments: 32 }, scene);
sphere.position.y = 5;

camera.setTarget(sphere.position);

if (props.environmentTexture) {
scene.environmentTexture = props.environmentTexture;
}

import("fs-extra").then(({ readJSON }) => {
readJSON(props.absolutePath)
.then((data) => {
try {
const rootUrl = projectConfiguration.path ? projectConfiguration.path.substring(0, projectConfiguration.path.lastIndexOf("/") + 1) : "";

const materialData = Material.Parse(data, scene, rootUrl);
sphere.material = materialData;
} catch (e) {
console.error("Failed to parse material:", e);
setLoadError(true);
}
})
.catch((e) => {
console.error("Failed to read material file:", e);
setLoadError(true);
});
});

engine.runRenderLoop(() => {
scene.render();
});

// Cleanup
return () => {
engine.stopRenderLoop();
scene.dispose();
engine.dispose();
};
}, [props.absolutePath]);

if (loadError) {
return <GiMaterialsScience size="64px" />;
}

return <canvas ref={canvasRef} width={width} height={height} className="w-full h-full object-contain rounded-md" />;
}