Compare commits

...
15 changed files with 1057 additions and 99 deletions
+35 -33
View File
@@ -1,40 +1,42 @@
<!doctype html>
<html lang="en">
<head>
<title>ArtCraft</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Y4Y8KMLPQ7"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-Y4Y8KMLPQ7');
</script>
<head>
<title>ArtCraft</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Microsoft Clarity -->
<script type="text/javascript">
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "sixp9uu263");
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Y4Y8KMLPQ7"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-Y4Y8KMLPQ7');
</script>
<script crossOrigin="anonymous" src="//unpkg.com/react-scan/dist/auto.global.js"></script>
<!-- Microsoft Clarity -->
<script type="text/javascript">
(function (c, l, a, r, i, t, y) {
c[a] = c[a] || function () {(c[a].q = c[a].q || []).push(arguments)};
t = l.createElement(r); t.async = 1; t.src = "https://www.clarity.ms/tag/" + i;
y = l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t, y);
})(window, document, "clarity", "script", "sixp9uu263");
</script>
<base href="/" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap"
rel="stylesheet"
/>
<!-- <link rel="stylesheet" href="/src/styles.css" /> -->
</head>
<body className="overflow-hidden bg-ui-background">
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
<base href="/" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap"
rel="stylesheet" />
<!-- <link rel="stylesheet" href="/src/styles.css" /> -->
</head>
<body className="overflow-hidden bg-ui-background">
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
@@ -2,6 +2,8 @@ import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { FontLoader } from "three/addons/loaders/FontLoader.js";
import { TextGeometry } from "three/addons/geometries/TextGeometry.js";
import { SplatFileType, SplatMesh } from "@sparkjsdev/spark";
import { SPLAT_FILE_TYPE } from "~/enums";
interface LoaderInterface {
file: File;
@@ -11,6 +13,10 @@ interface LoaderInterface {
statusCallback: (statusObject: { type: string; message?: string }) => void;
}
interface PreviewReturn {
renderer: THREE.WebGLRenderer;
}
export const loadPreviewOnCanvas = ({
file,
canvas,
@@ -19,7 +25,7 @@ export const loadPreviewOnCanvas = ({
file: File;
canvas: HTMLCanvasElement;
statusCallback: (error: { type: string; message?: string }) => void;
}) => {
}): PreviewReturn => {
// Setup of scene, camera, and renderer in the canvas
const scene = new THREE.Scene();
@@ -32,10 +38,13 @@ export const loadPreviewOnCanvas = ({
const camera = new THREE.PerspectiveCamera(35, aspectRatio, 0.1, 1000);
camera.position.z = 2;
const gl2ctx = canvas.getContext("webgl2");
const renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true,
canvas: canvas,
context: gl2ctx!,
preserveDrawingBuffer: true,
});
@@ -51,19 +60,37 @@ export const loadPreviewOnCanvas = ({
scene.add(light);
let splatMesh: SplatMesh | null = null;
// load the file into the preview mini-scene depending of the file's type
if (file.name.includes(".glb")) {
if (file.name.endsWith(".glb")) {
glbLoader({ file, scene, camera, renderer, statusCallback });
} else if (file.name.includes(".pmd")) {
} else if (file.name.endsWith(".pmd")) {
pmdLoader({ file, scene, camera, renderer, statusCallback });
} else if (
file.name.includes(".png") ||
file.name.includes(".jpg") ||
file.name.includes(".jpeg") ||
file.name.includes(".gif")
file.name.endsWith(".png") ||
file.name.endsWith(".jpg") ||
file.name.endsWith(".jpeg") ||
file.name.endsWith(".gif")
) {
imagePlaneLoader({ file, scene, camera, renderer, statusCallback });
} else if (file.name.includes(".vmd")) {
} else if (file.name.endsWith(".spz")) {
file.arrayBuffer().then((arrayBuffer) => {
splatMesh = new SplatMesh({
fileBytes: arrayBuffer, fileType: SplatFileType.SPZ,
onLoad: () => { scene.add(splatMesh!) }
});
if (file.name.split(".")[0].endsWith("ceramic")) {
splatMesh.rotateX(Math.PI);
}
}).catch((loaderError) => {
statusCallback({
type: "SPLAT Loader Error",
message: String(loaderError),
});
});
} else if (file.name.endsWith(".vmd")) {
statusCallback({
type: "Preview Error",
message: "Sorry, Preview is not available to VMD files yet",
@@ -78,8 +105,11 @@ export const loadPreviewOnCanvas = ({
// Render the loaded data
const animate = function () {
renderer.render(scene, camera);
splatMesh?.rotateY(0.01);
};
renderer.setAnimationLoop(animate);
return { renderer };
};
const glbLoader = ({
@@ -0,0 +1,198 @@
import { useCallback, useEffect, useState, useRef } from "react";
import { ListDropdown } from "@storyteller/ui-list-dropdown";
import { Button } from "@storyteller/ui-button";
import { FileUploader } from "@storyteller/ui-file-uploader";
import { UploaderState } from "../../../models";
import { FilterEngineCategories, MediaFileAnimationType, UploaderStates } from "../../../enums";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCube } from "@fortawesome/pro-solid-svg-icons";
import { loadPreviewOnCanvas, snapshotCanvasAsThumbnail } from "../UploadModal3D/utilities";
import { WebGLRenderer } from "three";
interface Props {
title: string;
fileTypes: string[];
engineCategory: FilterEngineCategories;
options?: {
fileSubtypes?: { [key: string]: string }[];
hasLength?: boolean;
hasThumbnailUpload?: boolean;
};
onClose: () => void;
onUploadProgress: (newState: UploaderState) => void;
onLocalBytes: (bytes: ArrayBuffer, shouldFlip) => void;
}
export const UploadFilesSplat = ({
fileTypes,
engineCategory,
options,
onClose,
onUploadProgress,
onLocalBytes,
}: Props) => {
const canvasRef = useRef<HTMLCanvasElement | undefined>(undefined);
const canvasCallbackRef = useCallback((node: HTMLCanvasElement) => {
if (node !== null) {
canvasRef.current = node;
}
}, []);
const fileSubtypes = options?.fileSubtypes;
// const hasLength = options?.hasLength;
// const hasThumbnailUpload = options?.hasThumbnailUpload;
const [subtype, setSubtype] = useState<MediaFileAnimationType | undefined>(
fileSubtypes
? (Object.values(fileSubtypes[0])[0] as MediaFileAnimationType)
: undefined,
);
const [assetFile, setAssetFile] = useState<{
value: File | null;
error?: string;
}>({ value: null });
const [previewStatus, setPreviewStatus] = useState<{
type: string;
message?: string;
}>({ type: "init" });
const [thumbnailFile, setThumbnailFile] = useState<Blob | undefined>(
undefined,
);
const handleSubmit = () => {
if (!assetFile.value) {
setAssetFile((curr) => ({
...curr,
error: "Please select a file to upload.",
}));
return;
}
// Use the filename as the title
const title = assetFile.value.name.split(".")[0];
const shouldFlip = title.endsWith("ceramic");
assetFile.value.arrayBuffer().then((arrayBuffer) => {
onLocalBytes(arrayBuffer, shouldFlip);
}).catch((error) => {
console.error("Error reading file as ArrayBuffer:", error);
});
// upload3DObjects({
// title: title,
// assetFile: assetFile.value,
// thumbnailSnapshot: thumbnailFile,
// engineCategory: engineCategory,
// animationType: subtype,
// progressCallback: onUploadProgress,
// });
};
useEffect(() => {
// Reset subtype when fileSubtypes set changes (e.g., toggling character)
if (!fileSubtypes || fileSubtypes.length === 0) {
setSubtype(undefined);
return;
}
const nextDefault = Object.values(fileSubtypes[0])[0] as
| MediaFileAnimationType
| undefined;
setSubtype(nextDefault);
}, [fileSubtypes]);
useEffect(() => {
let renderer: WebGLRenderer | null = null;
if (canvasRef.current && assetFile.value) {
renderer = loadPreviewOnCanvas({
file: assetFile.value,
canvas: canvasRef.current,
statusCallback: (statusObject: { type: string; message?: string }) => {
setPreviewStatus(statusObject);
},
}).renderer;
}
return () => {
renderer?.setAnimationLoop(null);
renderer?.dispose();
}
}, [assetFile.value]);
useEffect(() => {
if (previewStatus.type === "OK" && canvasRef.current) {
snapshotCanvasAsThumbnail({
targetNode: canvasRef.current!,
resultCallback: (snapshotBlob) => {
if (snapshotBlob) {
setThumbnailFile(snapshotBlob);
}
},
});
}
}, [previewStatus]);
return (
<>
<div className="flex flex-col gap-3">
<FileUploader
fileTypes={fileTypes}
file={assetFile.value ?? undefined}
handleChange={(file: File) => {
setAssetFile({
value: file,
});
}}
/>
{fileSubtypes && fileSubtypes.length > 1 && (
<ListDropdown
list={fileSubtypes}
onSelect={(value) => setSubtype(value as MediaFileAnimationType)}
/>
)}
{assetFile.error && (
<h6 className="z-10 text-red">{assetFile.error}</h6>
)}
<div className="relative m-auto w-full overflow-hidden rounded-lg bg-brand-secondary">
<canvas
className="pointer-events-none h-full w-full"
ref={canvasCallbackRef}
/>
{!assetFile.value && (
<h6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 flex w-full items-center justify-center gap-2.5 text-center opacity-50">
<FontAwesomeIcon icon={faCube} />
Your model preview will appear here
</h6>
)}
{previewStatus.type.includes("Error") && (
<>
<h6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 w-full text-center">
{previewStatus.type}
{previewStatus.message && <br />}
{previewStatus.message}
</h6>
</>
)}
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={onClose}>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSubmit}
disabled={!assetFile.value}
>
Upload
</Button>
</div>
</div>
</>
);
};
@@ -0,0 +1,143 @@
import { useEffect, useMemo, useState } from "react";
import { LoadingDots } from "@storyteller/ui-loading";
import { Modal } from "@storyteller/ui-modal";
import { UploadAssetError, UploadSuccess } from "@storyteller/ui-upload-modal";
import { initialUploaderState, UploaderState } from "../../../models";
import {
FilterEngineCategories,
UploaderStates,
MediaFileAnimationType,
SPLAT_FILE_TYPE,
} from "../../../enums";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import {
galleryModalVisibleViewMode,
galleryModalVisibleDuringDrag,
} from "@storyteller/ui-gallery-modal";
import { UploadFilesSplat } from "./UploadFilesSplat";
interface Props {
onClose: () => void;
onSuccess: (splatArrayBuffer: ArrayBuffer, shouldFlip: boolean) => void;
isOpen: boolean;
title: string;
titleIcon: IconDefinition;
options?: {
fileSubtypes?: { [key: string]: string }[];
hasLength?: boolean;
hasThumbnailUpload?: boolean;
};
}
const splatFileTypes = Object.values(SPLAT_FILE_TYPE);
export function UploadModalSplat(props: Props) {
const selectedCategory = FilterEngineCategories.SPLAT;
const { isOpen, onClose, onSuccess, title, titleIcon, options } = props;
const [uploaderState, setUploaderState] =
useState<UploaderState>(initialUploaderState);
const updateUploaderState = (newState: UploaderState) => {
setUploaderState(newState);
};
const resetModalState = () => {
setUploaderState(initialUploaderState);
};
useEffect(() => {
if (isOpen) {
resetModalState();
}
}, [isOpen]);
useEffect(() => {
if (uploaderState.status === UploaderStates.success) {
// Automatically open the global Gallery modal after a successful upload
// galleryModalVisibleViewMode.value = true;
// galleryModalVisibleDuringDrag.value = true;
// onSuccess(selectedCategory);
onClose();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uploaderState.status]);
const UploaderModalContent = () => {
switch (uploaderState.status) {
case UploaderStates.ready:
return (
<div className="space-y-4">
<UploadFilesSplat
title={title}
engineCategory={selectedCategory}
fileTypes={splatFileTypes}
options={{
...(options ?? {}),
}}
onClose={onClose}
onUploadProgress={updateUploaderState}
onLocalBytes={(buffer, shouldFlip) => { onSuccess(buffer, shouldFlip); }}
/>
</div>
);
case UploaderStates.uploadingAsset:
case UploaderStates.uploadingCover:
case UploaderStates.settingCover:
return (
<>
<LoadingDots className="mb-1 bg-transparent" />
<div className="w-100 text-center opacity-50">Uploading...</div>
</>
);
case UploaderStates.success: {
return (
<UploadSuccess
title="Splat"
onOk={() => {
onClose();
// onSuccess(selectedCategory);
}}
/>
);
}
case UploaderStates.assetError:
return (
<UploadAssetError
onCancel={onClose}
onRetry={() => {
resetModalState();
}}
type={selectedCategory}
errorMessage={uploaderState.errorMessage}
/>
);
case UploaderStates.coverCreateError:
case UploaderStates.coverSetError:
return (
<UploadAssetError
onCancel={onClose}
onRetry={() => {
resetModalState();
}}
type={"Thumbnail"}
errorMessage={uploaderState.errorMessage}
/>
);
}
return undefined;
};
return (
<Modal
isOpen={isOpen}
onClose={onClose}
titleIcon={titleIcon}
title={title}
className="max-w-xl"
showClose={true}
>
<UploaderModalContent />
</Modal>
);
}
@@ -0,0 +1 @@
export * from "./UploadModalSplat";
@@ -1,2 +1,3 @@
export * from "./UploadModal3D";
export * from "./UploadModalMedia";
export * from "./UploadModalSplat";
@@ -42,6 +42,10 @@ export enum OBJECT_FILE_TYPE {
GLB = "glb",
}
export enum SPLAT_FILE_TYPE {
SPZ = "spz",
}
export enum CHARACTER_MMD_FILE_TYPE {
PMD = "pmd",
PMX = "pmx",
@@ -23,6 +23,7 @@ export enum FilterEngineCategories {
IMAGE_PLANE = "image_plane",
LOCATION = "location",
OBJECT = "object",
SPLAT = "splat",
SCENE = "scene",
SET_DRESSING = "set_dressing",
SKYBOX = "skybox",
@@ -53,6 +53,8 @@ import FindSurfaces from "./FindSurfaces.js";
import Stats from "three/examples/jsm/libs/stats.module.js";
import { CharacterAnimationEngine } from "./Engines/CharacterAnimationEngine";
import { cameras, selectedCameraId } from "~/pages/PageEnigma/signals/camera";
import { SparkRenderer } from "@sparkjsdev/spark";
import { SSAOPass } from "three/examples/jsm/Addons.js";
export type EditorInitializeConfig = {
sceneToken: string;
@@ -89,6 +91,7 @@ class Editor {
render_camera_aspect_ratio: CameraAspectRatio =
CameraAspectRatio.HORIZONTAL_3_2;
renderer: THREE.WebGLRenderer | undefined;
sparkRenderer: SparkRenderer | null = null;
rawRenderer: THREE.WebGLRenderer | undefined;
clock: THREE.Clock | undefined;
canvReference: HTMLCanvasElement | null = null;
@@ -99,7 +102,7 @@ class Editor {
outlinePass: OutlinePass | undefined;
last_cam_pos: THREE.Vector3;
last_cam_rot: THREE.Euler;
saoPass: SAOPass | undefined;
ssaoPass: SSAOPass | undefined;
outputPass: OutputPass | undefined;
renderOutputPass: OutputPass | undefined;
bloomPass: UnrealBloomPass | undefined;
@@ -273,7 +276,7 @@ class Editor {
// Recording params.
this.render_timer = 0;
this.fps_number = 60;
this.cap_fps = 120;
this.cap_fps = 60;
// Timeline settings.
this.can_playback = false;
this.playback_location = 0;
@@ -502,8 +505,8 @@ class Editor {
this.camera = new THREE.PerspectiveCamera(
this.focalLengthToFov(mainCameraConfig.focalLength),
width / height,
0.01,
200,
0.1,
2000,
);
this.camera.position.set(
mainCameraConfig.position.x,
@@ -552,6 +555,11 @@ class Editor {
preserveDrawingBuffer: true,
});
// this.sparkRenderer = new SparkRenderer({
// renderer: this.renderer,
// autoUpdate: true,
// });
this.rawRenderer = new THREE.WebGLRenderer({
antialias: true,
canvas: this.canvasRenderCamReference,
@@ -667,6 +675,10 @@ class Editor {
// Attach freecam state controller
this.mouseOverEventHandler = this.mouseOverEventHandler.bind(this);
// Add spark renderer as a child of the camera
this.activeScene.scene.add(this.sparkRenderer);
this.camera?.add(this.sparkRenderer);
const onloadCallback = () => {
console.log("Setting Scene is loaded");
this._isEngineDataLoaded = true;
@@ -1145,26 +1157,19 @@ class Editor {
this.composer.addPass(this.renderPass);
this.outlinePass = new OutlinePass(
new THREE.Vector2(width, height),
new THREE.Vector2(width / 10, height / 10),
this.activeScene.scene,
this.camera,
);
this.outlinePass.edgeStrength = 5.0;
this.outlinePass.edgeGlow = 0.1;
this.outlinePass.edgeThickness = 1.2;
this.outlinePass.pulsePeriod = 3;
this.outlinePass.usePatternTexture = false;
this.outlinePass.visibleEdgeColor.set(0x4b9fff);
// this.outlinePass.edgeStrength = 5.0;
// this.outlinePass.edgeGlow = 0.1;
// this.outlinePass.edgeThickness = 1.2;
// this.outlinePass.pulsePeriod = 3;
// this.outlinePass.usePatternTexture = false;
// this.outlinePass.visibleEdgeColor.set(0x4b9fff);
this.composer.addPass(this.outlinePass);
this.saoPass = new SAOPass(this.activeScene.scene, this.camera);
this.saoPass.params.saoBias = 4.1;
this.saoPass.params.saoIntensity = 1.0;
this.saoPass.params.saoScale = 32.0;
this.saoPass.params.saoKernelRadius = 5.0;
this.saoPass.params.saoMinResolution = 0.0;
// this.composer.addPass(this.outlinePass);
this.bloomPass = new UnrealBloomPass(
new THREE.Vector2(width, height),
@@ -1174,14 +1179,16 @@ class Editor {
);
this.bloomPass.strength = 0.25;
this.smaaPass = new SMAAPass(
width * this.renderer.getPixelRatio(),
height * this.renderer.getPixelRatio(),
);
// this.smaaPass = new SMAAPass(
// width * this.renderer.getPixelRatio(),
// height * this.renderer.getPixelRatio(),
// );
this.composer.addPass(this.saoPass);
this.composer.addPass(this.bloomPass);
//this.composer.addPass(this.smaaPass);
// this.composer.addPass(this.bloomPass);
// this.composer.addPass(this.smaaPass);
// this.ssaoPass = new SSAOPass(this.activeScene.scene, this.camera, width, height);
// this.composer.addPass(this.ssaoPass);
this.outputPass = new OutputPass();
this.composer.addPass(this.outputPass);
@@ -1328,7 +1335,10 @@ class Editor {
this.rawRenderer &&
this.render_composer
) {
const then = performance.now();
this.composer.render();
const now = performance.now();
console.log("Performance: ", now - then);
//this.rawRenderer.render(this.activeScene.scene, this.render_camera!);
// this.render_composer.render();
} else if (this.renderer && this.render_camera && !this.rendering) {
@@ -1337,7 +1347,7 @@ class Editor {
} else if (this.rendering && this.renderer) {
this.renderer.setSize(this.render_width, this.render_height);
}
await this.recordScene();
// await this.recordScene();
}
async useCachedMediaTokens(): Promise<boolean> {
@@ -24,6 +24,7 @@ import { Vector3 } from "three";
import { filmLength, outlinerState } from "../signals";
import { CharacterAnimationEngine } from "./Engines/CharacterAnimationEngine";
import { SplatMesh } from "@sparkjsdev/spark";
export class TimeLine {
editorEngine: Editor;
@@ -395,6 +396,18 @@ export class TimeLine {
return obj;
}
// This method is for local dev testing of objects
// and bypasses the queue system, that's kinda redundant anyway
public addLocalSplat(fileBytes: ArrayBuffer | Uint8Array, flipVertical: boolean = false) {
const splatMesh = new SplatMesh({ fileBytes: fileBytes });
if (flipVertical) {
splatMesh.rotateX(Math.PI);
}
this.editorEngine.sceneManager?.scene.scene.add(splatMesh);
}
public async addShape(data: MediaItem) {
const pos = this.getPos();
const parim = await this.editorEngine.create_parim(data.media_id, pos);
@@ -36,6 +36,7 @@ import {
} from "@storyteller/ui-gallery-modal";
import { twMerge } from "tailwind-merge";
import { UploadModalImage } from "../../../../components/reusable/UploadModalImage";
import { UploadModalSplat } from "~/components/reusable/UploadModalSplat";
export const Controls3D = () => {
useSignals();
@@ -46,6 +47,7 @@ export const Controls3D = () => {
const [upload3DIsShowing, setUpload3DIsShowing] = useState(false);
const [isAddAssetPopoverOpen, setIsAddAssetPopoverOpen] = useState(false);
const [uploadImageIsShowing, setUploadImageIsShowing] = useState(false);
const [uploadSplatIsShowing, setUploadSplatIsShowing] = useState(false);
useEffect(() => {
@@ -57,7 +59,8 @@ export const Controls3D = () => {
!galleryModalVisibleViewMode.value &&
!isAddAssetPopoverOpen &&
!upload3DIsShowing &&
!uploadImageIsShowing;
!uploadImageIsShowing &&
!uploadSplatIsShowing;
setShowEmptySceneTooltip(isSceneEmpty);
};
@@ -78,6 +81,7 @@ export const Controls3D = () => {
isAddAssetPopoverOpen,
upload3DIsShowing,
uploadImageIsShowing,
uploadSplatIsShowing,
]);
const handleModeChange = (value: string) => {
@@ -170,6 +174,9 @@ export const Controls3D = () => {
case "upload-image":
setUploadImageIsShowing(true);
break;
case "upload-splat":
setUploadSplatIsShowing(true);
break;
default:
break;
}
@@ -268,14 +275,24 @@ export const Controls3D = () => {
),
action: "upload-image",
},
{
label: "Upload Splat",
selected: false,
icon: (
<FontAwesomeIcon
icon={faArrowUpFromBracket}
className="h-4 w-4"
/>
),
action: "upload-splat",
},
]}
onPanelAction={handleAddAssetAction}
showIconsInList
buttonClassName={`h-9 w-9 rounded-[10px] text-lg ${
showEmptySceneTooltip
? "bg-primary/90 hover:bg-primary/70"
: "border-transparent bg-primary/90 hover:bg-primary/70"
}`}
buttonClassName={`h-9 w-9 rounded-[10px] text-lg ${showEmptySceneTooltip
? "bg-primary/90 hover:bg-primary/70"
: "border-transparent bg-primary/90 hover:bg-primary/70"
}`}
triggerIcon={
<FontAwesomeIcon icon={faPlus} className="text-xl" />
}
@@ -331,6 +348,17 @@ export const Controls3D = () => {
title="Upload an Image"
titleIcon={faImages}
/>
<UploadModalSplat
isOpen={uploadSplatIsShowing}
onClose={() => setUploadSplatIsShowing(false)}
onSuccess={(buffer, shouldFlip) => {
setUploadSplatIsShowing(false)
editorEngine?.timeline.addLocalSplat(buffer, shouldFlip);
}}
title="Upload an spz file"
titleIcon={faCube}
/>
</>
);
};
-1
View File
@@ -73,7 +73,6 @@
"react-virtualized": "^9.22.6",
"tailwind-merge": "^2.2.2",
"terser": "^5.30.3",
"three": "^0.162.0",
"uuid": "^9.0.1",
"wavesurfer.js": "^7.7.11",
"webm-writer": "^1.0.0"
+50 -10
View File
@@ -1,25 +1,65 @@
import { defineConfig } from "vite";
import { defineConfig, type Plugin } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import path from 'path'
import { resolve } from 'node:path'
import path from "path";
import { resolve } from "node:path";
import wasm from "vite-plugin-wasm";
// NB(bt): This configuration file can specify bundler rollup options, compiler plugins,
// import path resolution, dev server HTTP headers, CORS options, path rewriting, etc.
// Read the vite docs for more: https://vitejs.dev/config/
const SPARK_MODULE_SUBPATH = "/@sparkjsdev/spark/dist/spark.module.js";
const SPARK_WASM_PATTERN =
/module_or_path = new URL\("(data:application\/wasm;base64,[^"]+)", import\.meta\.url\);/;
const sparkWasmDataUrlFix = (): Plugin => ({
name: "spark-wasm-data-url-fix",
enforce: "pre",
apply: "serve",
transform(code, id) {
if (!id.includes(SPARK_MODULE_SUBPATH)) {
return null;
}
const match = code.match(SPARK_WASM_PATTERN);
if (!match) {
return null;
}
const [, dataUrl] = match;
const patched = code.replace(
SPARK_WASM_PATTERN,
`module_or_path = "${dataUrl}";`,
);
return {
code: patched,
map: null,
};
},
});
const projectRoot = __dirname;
const appRoot = path.resolve(projectRoot, "app");
const workspaceRoot = path.resolve(projectRoot, "..", "..");
export default defineConfig({
root: path.resolve(__dirname, 'app'),
root: appRoot,
optimizeDeps: {
exclude: ["@sparkjsdev/spark"],
},
build: {
outDir: path.resolve(__dirname, 'dist'),
outDir: path.resolve(projectRoot, "dist"),
rollupOptions: {
input: {
index: resolve(__dirname, 'app/index.html'),
index: resolve(projectRoot, "app/index.html"),
},
},
},
plugins: [
tsconfigPaths(),
],
plugins: [sparkWasmDataUrlFix(), tsconfigPaths(), wasm()],
server: {
}
fs: {
allow: [workspaceRoot],
},
},
});
+498 -13
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -17,6 +17,7 @@
"@preact/signals-react": "^3.0.1",
"@radix-ui/react-dialog": "^1.1.14",
"@remix-run/serve": "^2.15.0",
"@sparkjsdev/spark": "^0.1.10",
"dayjs": "^1.11.10",
"detect-gpu": "^5.0.38",
"embla-carousel-react": "^8.6.0",
@@ -36,7 +37,7 @@
"react-router-dom": "6.29.0",
"react-spring": "^10.0.1",
"tailwind-merge": "^2.2.2",
"three": "^0.162.0",
"three": "^0.171.0",
"uuid": "^9.0.1",
"zustand": "^5.0.5"
},
@@ -93,6 +94,8 @@
"vite-plugin-banner": "^0.8.0",
"vite-plugin-dts": "~4.5.0",
"vite-plugin-static-copy": "^2.3.1",
"vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.5.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.0"
},