react-three-game 0.0.38 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/helpers/SoundManager.d.ts +35 -0
- package/dist/helpers/SoundManager.js +93 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/shared/GameCanvas.d.ts +6 -3
- package/dist/shared/GameCanvas.js +4 -4
- package/dist/tools/loading/GameWithLoader.d.ts +6 -0
- package/dist/tools/loading/GameWithLoader.js +8 -0
- package/dist/tools/loading/loading.d.ts +2 -0
- package/dist/tools/loading/loading.js +38 -0
- package/dist/tools/prefabeditor/EditorUI.js +11 -11
- package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +3 -1
- package/dist/tools/prefabeditor/components/MaterialComponent.js +23 -4
- package/dist/tools/prefabeditor/components/ModelComponent.js +2 -2
- package/package.json +1 -1
- package/src/helpers/SoundManager.ts +130 -0
- package/src/index.ts +1 -0
- package/src/shared/GameCanvas.tsx +9 -3
- package/src/tools/prefabeditor/EditorUI.tsx +12 -4
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +3 -1
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +77 -5
- package/src/tools/prefabeditor/components/ModelComponent.tsx +3 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
declare class SoundManager {
|
|
2
|
+
private static _instance;
|
|
3
|
+
context: AudioContext;
|
|
4
|
+
private buffers;
|
|
5
|
+
private masterGain;
|
|
6
|
+
private sfxGain;
|
|
7
|
+
private musicGain;
|
|
8
|
+
private constructor();
|
|
9
|
+
/** Singleton accessor */
|
|
10
|
+
static get instance(): SoundManager;
|
|
11
|
+
/** Required once after user gesture (browser) */
|
|
12
|
+
resume(): void;
|
|
13
|
+
/** Preload a sound from URL */
|
|
14
|
+
load(path: string, url: string): Promise<void>;
|
|
15
|
+
/** Play from already-loaded buffer (fails silently if not loaded) */
|
|
16
|
+
playSync(path: string, { volume, playbackRate, detune, pitch, }?: {
|
|
17
|
+
volume?: number;
|
|
18
|
+
playbackRate?: number;
|
|
19
|
+
detune?: number;
|
|
20
|
+
pitch?: number;
|
|
21
|
+
}): void;
|
|
22
|
+
/** Load and play SFX - accepts file path directly */
|
|
23
|
+
play(path: string, options?: {
|
|
24
|
+
volume?: number;
|
|
25
|
+
playbackRate?: number;
|
|
26
|
+
detune?: number;
|
|
27
|
+
pitch?: number;
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
/** Volume controls */
|
|
30
|
+
setMasterVolume(v: number): void;
|
|
31
|
+
setSfxVolume(v: number): void;
|
|
32
|
+
setMusicVolume(v: number): void;
|
|
33
|
+
}
|
|
34
|
+
export declare const sound: SoundManager;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
class SoundManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.buffers = new Map();
|
|
13
|
+
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
14
|
+
this.context = new AudioCtx();
|
|
15
|
+
this.masterGain = this.context.createGain();
|
|
16
|
+
this.sfxGain = this.context.createGain();
|
|
17
|
+
this.musicGain = this.context.createGain();
|
|
18
|
+
this.sfxGain.connect(this.masterGain);
|
|
19
|
+
this.musicGain.connect(this.masterGain);
|
|
20
|
+
this.masterGain.connect(this.context.destination);
|
|
21
|
+
this.masterGain.gain.value = 1;
|
|
22
|
+
this.sfxGain.gain.value = 1;
|
|
23
|
+
this.musicGain.gain.value = 1;
|
|
24
|
+
}
|
|
25
|
+
/** Singleton accessor */
|
|
26
|
+
static get instance() {
|
|
27
|
+
if (typeof window === 'undefined') {
|
|
28
|
+
// Return a dummy instance for SSR
|
|
29
|
+
return new Proxy({}, {
|
|
30
|
+
get: () => () => { }
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (!SoundManager._instance) {
|
|
34
|
+
SoundManager._instance = new SoundManager();
|
|
35
|
+
}
|
|
36
|
+
return SoundManager._instance;
|
|
37
|
+
}
|
|
38
|
+
/** Required once after user gesture (browser) */
|
|
39
|
+
resume() {
|
|
40
|
+
if (this.context.state !== "running") {
|
|
41
|
+
this.context.resume();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Preload a sound from URL */
|
|
45
|
+
load(path, url) {
|
|
46
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
+
if (this.buffers.has(path))
|
|
48
|
+
return;
|
|
49
|
+
const res = yield fetch(url);
|
|
50
|
+
const arrayBuffer = yield res.arrayBuffer();
|
|
51
|
+
const buffer = yield this.context.decodeAudioData(arrayBuffer);
|
|
52
|
+
this.buffers.set(path, buffer);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** Play from already-loaded buffer (fails silently if not loaded) */
|
|
56
|
+
playSync(path, { volume = 1, playbackRate = 1, detune = 0, pitch = 1, } = {}) {
|
|
57
|
+
this.resume();
|
|
58
|
+
const buffer = this.buffers.get(path);
|
|
59
|
+
if (!buffer)
|
|
60
|
+
return;
|
|
61
|
+
const src = this.context.createBufferSource();
|
|
62
|
+
const gain = this.context.createGain();
|
|
63
|
+
src.buffer = buffer;
|
|
64
|
+
src.playbackRate.value = playbackRate * pitch;
|
|
65
|
+
src.detune.value = detune;
|
|
66
|
+
gain.gain.value = volume;
|
|
67
|
+
src.connect(gain);
|
|
68
|
+
gain.connect(this.sfxGain);
|
|
69
|
+
src.start();
|
|
70
|
+
}
|
|
71
|
+
/** Load and play SFX - accepts file path directly */
|
|
72
|
+
play(path, options) {
|
|
73
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
74
|
+
// Auto-load from path if not already loaded
|
|
75
|
+
if (!this.buffers.has(path)) {
|
|
76
|
+
yield this.load(path, path);
|
|
77
|
+
}
|
|
78
|
+
this.playSync(path, options);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Volume controls */
|
|
82
|
+
setMasterVolume(v) {
|
|
83
|
+
this.masterGain.gain.value = v;
|
|
84
|
+
}
|
|
85
|
+
setSfxVolume(v) {
|
|
86
|
+
this.sfxGain.gain.value = v;
|
|
87
|
+
}
|
|
88
|
+
setMusicVolume(v) {
|
|
89
|
+
this.musicGain.gain.value = v;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
SoundManager._instance = null;
|
|
93
|
+
export const sound = SoundManager.instance;
|
package/dist/index.d.ts
CHANGED
|
@@ -10,4 +10,5 @@ export * as editorStyles from './tools/prefabeditor/styles';
|
|
|
10
10
|
export * from './tools/prefabeditor/utils';
|
|
11
11
|
export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
|
|
12
12
|
export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
|
|
13
|
+
export { sound as soundManager } from './helpers/SoundManager';
|
|
13
14
|
export * from './helpers';
|
package/dist/index.js
CHANGED
|
@@ -9,5 +9,6 @@ export * from './tools/prefabeditor/utils';
|
|
|
9
9
|
// Asset Tools
|
|
10
10
|
export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
|
|
11
11
|
export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
|
|
12
|
+
export { sound as soundManager } from './helpers/SoundManager';
|
|
12
13
|
// Helpers
|
|
13
14
|
export * from './helpers';
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { CanvasProps } from "@react-three/fiber";
|
|
1
2
|
import { WebGPURendererParameters } from "three/src/renderers/webgpu/WebGPURenderer.Nodes.js";
|
|
2
|
-
|
|
3
|
+
interface GameCanvasProps extends Omit<CanvasProps, 'children'> {
|
|
3
4
|
loader?: boolean;
|
|
4
5
|
children: React.ReactNode;
|
|
5
|
-
|
|
6
|
-
}
|
|
6
|
+
glConfig?: WebGPURendererParameters;
|
|
7
|
+
}
|
|
8
|
+
export default function GameCanvas({ loader, children, glConfig, ...props }: GameCanvasProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -32,17 +32,17 @@ extend({
|
|
|
32
32
|
SpriteNodeMaterial: SpriteNodeMaterial,
|
|
33
33
|
});
|
|
34
34
|
export default function GameCanvas(_a) {
|
|
35
|
-
var { loader = false, children } = _a, props = __rest(_a, ["loader", "children"]);
|
|
35
|
+
var { loader = false, children, glConfig } = _a, props = __rest(_a, ["loader", "children", "glConfig"]);
|
|
36
36
|
const [frameloop, setFrameloop] = useState("never");
|
|
37
|
-
return _jsx(_Fragment, { children: _jsxs(Canvas, { style: { touchAction: 'none' }, shadows: { type: PCFShadowMap, }, frameloop: frameloop, gl: (_a) => __awaiter(this, [_a], void 0, function* ({ canvas }) {
|
|
37
|
+
return _jsx(_Fragment, { children: _jsxs(Canvas, Object.assign({ style: { touchAction: 'none' }, shadows: { type: PCFShadowMap, }, frameloop: frameloop, gl: (_a) => __awaiter(this, [_a], void 0, function* ({ canvas }) {
|
|
38
38
|
const renderer = new WebGPURenderer(Object.assign({ canvas: canvas,
|
|
39
39
|
// @ts-expect-error futuristic
|
|
40
|
-
shadowMap: true, antialias: true },
|
|
40
|
+
shadowMap: true, antialias: true }, glConfig));
|
|
41
41
|
yield renderer.init().then(() => {
|
|
42
42
|
setFrameloop("always");
|
|
43
43
|
});
|
|
44
44
|
return renderer;
|
|
45
45
|
}), camera: {
|
|
46
46
|
position: [0, 1, 5],
|
|
47
|
-
}, children: [_jsx(Suspense, { children: children }), loader ? _jsx(Loader, {}) : null] }) });
|
|
47
|
+
} }, props, { children: [_jsx(Suspense, { children: children }), loader ? _jsx(Loader, {}) : null] })) });
|
|
48
48
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cloneElement, useState } from "react";
|
|
4
|
+
import LoadingSpinner from "../../sketches/loading/loading";
|
|
5
|
+
export default function GameWithLoader({ children }) {
|
|
6
|
+
const [isCanvasReady, setIsCanvasReady] = useState(false);
|
|
7
|
+
return (_jsxs(_Fragment, { children: [!isCanvasReady && _jsx(LoadingSpinner, {}), cloneElement(children, { onCanvasReady: () => setIsCanvasReady(true) })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
const lines = [
|
|
5
|
+
"$ initializing...",
|
|
6
|
+
"✓ loading scene",
|
|
7
|
+
"✓ ready"
|
|
8
|
+
];
|
|
9
|
+
const LoadingSpinner = () => {
|
|
10
|
+
const [index, setIndex] = useState(0);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (index < lines.length) {
|
|
13
|
+
const timer = setTimeout(() => setIndex(index + 1), 300);
|
|
14
|
+
return () => clearTimeout(timer);
|
|
15
|
+
}
|
|
16
|
+
}, [index]);
|
|
17
|
+
return (_jsxs("div", { className: "terminal-loading", children: [lines.slice(0, index).map((line, i) => (_jsx("div", { children: line }, i))), _jsx("div", { className: "cursor", children: "\u2588" }), _jsx("style", { jsx: true, children: `
|
|
18
|
+
.terminal-loading {
|
|
19
|
+
position: fixed;
|
|
20
|
+
inset: 0;
|
|
21
|
+
z-index: 50;
|
|
22
|
+
background: #0a0a0a;
|
|
23
|
+
color: #00ff00;
|
|
24
|
+
font-family: monospace;
|
|
25
|
+
padding: 2rem;
|
|
26
|
+
font-size: 14px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.cursor {
|
|
30
|
+
animation: blink 1s infinite;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@keyframes blink {
|
|
34
|
+
50% { opacity: 0; }
|
|
35
|
+
}
|
|
36
|
+
` })] }));
|
|
37
|
+
};
|
|
38
|
+
export default LoadingSpinner;
|
|
@@ -49,23 +49,23 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
49
49
|
if (!newAvailable.includes(addType))
|
|
50
50
|
setAddType(newAvailable[0] || "");
|
|
51
51
|
}, [Object.keys(node.components || {}).join(',')]);
|
|
52
|
-
return _jsxs("div", { style: inspector.content, className: "prefab-scroll", children: [_jsxs("div", { style: base.section, children: [_jsx("div", { style:
|
|
52
|
+
return _jsxs("div", { style: inspector.content, className: "prefab-scroll", children: [_jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }, children: [_jsx("div", { style: { fontSize: 10, color: '#888', wordBreak: 'break-all', border: '1px solid rgba(255,255,255,0.1)', padding: '2px 4px', borderRadius: 4, flex: 1 }, children: node.id }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), title: "Delete Node", onClick: deleteNode, children: "\u274C" })] }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", placeholder: 'Node name', onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsx("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: _jsx("div", { style: base.label, children: "Components" }) }), node.components && Object.entries(node.components).map(([key, comp]) => {
|
|
53
53
|
if (!comp)
|
|
54
54
|
return null;
|
|
55
55
|
const def = ALL_COMPONENTS[comp.type];
|
|
56
56
|
if (!def)
|
|
57
57
|
return _jsxs("div", { style: { color: '#ff8888', fontSize: 11 }, children: ["Unknown: ", comp.type] }, key);
|
|
58
|
-
return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 500 }, children: key }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px' }), onClick: () => updateNode(n => {
|
|
58
|
+
return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 500 }, children: key }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px' }), title: "Remove Component", onClick: () => updateNode(n => {
|
|
59
59
|
const _a = n.components || {}, _b = key, _ = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
|
|
60
60
|
return Object.assign(Object.assign({}, n), { components: rest });
|
|
61
|
-
}), children: "\u2715" })] }), def.Editor && (_jsx(def.Editor, { component: comp, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath, transformMode: transformMode, setTransformMode: setTransformMode }))] }, key));
|
|
62
|
-
})] }), available.length > 0 && (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
}), children: "\u2715" })] }), def.Editor && (_jsx(def.Editor, { component: comp, node: node, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath, transformMode: transformMode, setTransformMode: setTransformMode }))] }, key));
|
|
62
|
+
})] }), available.length > 0 && (_jsx("div", { children: _jsxs("div", { style: base.row, children: [_jsx("select", { style: Object.assign(Object.assign({}, base.input), { flex: 1 }), value: addType, onChange: e => setAddType(e.target.value), children: available.map(k => _jsx("option", { value: k, children: k }, k)) }), _jsx("button", { style: base.btn, disabled: !addType, onClick: () => {
|
|
63
|
+
if (!addType)
|
|
64
|
+
return;
|
|
65
|
+
const def = ALL_COMPONENTS[addType];
|
|
66
|
+
if (def) {
|
|
67
|
+
updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [addType.toLowerCase()]: { type: def.name, properties: def.defaultProperties } }) })));
|
|
68
|
+
}
|
|
69
|
+
}, title: "Add Component", children: "+" })] }) }))] });
|
|
70
70
|
}
|
|
71
71
|
export default EditorUI;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { FC } from "react";
|
|
2
|
+
import { ComponentData, GameObject } from "../types";
|
|
2
3
|
export interface Component {
|
|
3
4
|
name: string;
|
|
4
5
|
Editor: FC<{
|
|
5
|
-
|
|
6
|
+
node?: GameObject;
|
|
7
|
+
component: ComponentData;
|
|
6
8
|
onUpdate: (newComp: any) => void;
|
|
7
9
|
basePath?: string;
|
|
8
10
|
transformMode?: "translate" | "rotate" | "scale";
|
|
@@ -2,6 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { TextureListViewer } from '../../assetviewer/page';
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import { Input, Label } from './Input';
|
|
5
|
+
import { useMemo } from 'react';
|
|
6
|
+
import { DoubleSide, RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter } from 'three';
|
|
5
7
|
function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
6
8
|
var _a, _b, _c, _d;
|
|
7
9
|
const [textureFiles, setTextureFiles] = useState([]);
|
|
@@ -30,19 +32,33 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
|
30
32
|
var _a, _b;
|
|
31
33
|
const x = (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1;
|
|
32
34
|
onUpdate({ repeatCount: [x, value] });
|
|
33
|
-
} })] })] }))] }))] }));
|
|
35
|
+
} })] })] })), _jsxs("div", { style: { marginTop: 4 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 }, children: [_jsx("input", { type: "checkbox", style: { width: 12, height: 12 }, checked: component.properties.generateMipmaps !== false, onChange: e => onUpdate({ generateMipmaps: e.target.checked }) }), _jsx("label", { style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Generate Mipmaps" })] }), _jsxs("div", { children: [_jsx(Label, { children: "Min Filter" }), _jsxs("select", { style: Object.assign(Object.assign({}, textInputStyle), { width: '100%', cursor: 'pointer' }), value: component.properties.minFilter || 'LinearMipmapLinearFilter', onChange: e => onUpdate({ minFilter: e.target.value }), children: [_jsx("option", { value: "NearestFilter", children: "Nearest" }), _jsx("option", { value: "NearestMipmapNearestFilter", children: "Nearest Mipmap Nearest" }), _jsx("option", { value: "NearestMipmapLinearFilter", children: "Nearest Mipmap Linear" }), _jsx("option", { value: "LinearFilter", children: "Linear" }), _jsx("option", { value: "LinearMipmapNearestFilter", children: "Linear Mipmap Nearest" }), _jsx("option", { value: "LinearMipmapLinearFilter", children: "Linear Mipmap Linear (Default)" })] })] }), _jsxs("div", { style: { marginTop: 4 }, children: [_jsx(Label, { children: "Mag Filter" }), _jsxs("select", { style: Object.assign(Object.assign({}, textInputStyle), { width: '100%', cursor: 'pointer' }), value: component.properties.magFilter || 'LinearFilter', onChange: e => onUpdate({ magFilter: e.target.value }), children: [_jsx("option", { value: "NearestFilter", children: "Nearest" }), _jsx("option", { value: "LinearFilter", children: "Linear (Default)" })] })] })] })] }))] }));
|
|
34
36
|
}
|
|
35
37
|
;
|
|
36
|
-
import { useMemo } from 'react';
|
|
37
|
-
import { DoubleSide, RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace } from 'three';
|
|
38
38
|
// View for Material component
|
|
39
39
|
function MaterialComponentView({ properties, loadedTextures }) {
|
|
40
40
|
var _a;
|
|
41
41
|
const textureName = properties === null || properties === void 0 ? void 0 : properties.texture;
|
|
42
42
|
const repeat = properties === null || properties === void 0 ? void 0 : properties.repeat;
|
|
43
43
|
const repeatCount = properties === null || properties === void 0 ? void 0 : properties.repeatCount;
|
|
44
|
+
const generateMipmaps = (properties === null || properties === void 0 ? void 0 : properties.generateMipmaps) !== false;
|
|
45
|
+
const minFilter = (properties === null || properties === void 0 ? void 0 : properties.minFilter) || 'LinearMipmapLinearFilter';
|
|
46
|
+
const magFilter = (properties === null || properties === void 0 ? void 0 : properties.magFilter) || 'LinearFilter';
|
|
44
47
|
const texture = textureName && loadedTextures ? loadedTextures[textureName] : undefined;
|
|
48
|
+
const minFilterMap = {
|
|
49
|
+
NearestFilter,
|
|
50
|
+
LinearFilter,
|
|
51
|
+
NearestMipmapNearestFilter,
|
|
52
|
+
NearestMipmapLinearFilter,
|
|
53
|
+
LinearMipmapNearestFilter,
|
|
54
|
+
LinearMipmapLinearFilter
|
|
55
|
+
};
|
|
56
|
+
const magFilterMap = {
|
|
57
|
+
NearestFilter,
|
|
58
|
+
LinearFilter
|
|
59
|
+
};
|
|
45
60
|
const finalTexture = useMemo(() => {
|
|
61
|
+
var _a, _b;
|
|
46
62
|
if (!texture)
|
|
47
63
|
return undefined;
|
|
48
64
|
const t = texture.clone();
|
|
@@ -56,9 +72,12 @@ function MaterialComponentView({ properties, loadedTextures }) {
|
|
|
56
72
|
t.repeat.set(1, 1);
|
|
57
73
|
}
|
|
58
74
|
t.colorSpace = SRGBColorSpace;
|
|
75
|
+
t.generateMipmaps = generateMipmaps;
|
|
76
|
+
t.minFilter = (_a = minFilterMap[minFilter]) !== null && _a !== void 0 ? _a : LinearMipmapLinearFilter;
|
|
77
|
+
t.magFilter = (_b = magFilterMap[magFilter]) !== null && _b !== void 0 ? _b : LinearFilter;
|
|
59
78
|
t.needsUpdate = true;
|
|
60
79
|
return t;
|
|
61
|
-
}, [texture, repeat, repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[0], repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[1]]);
|
|
80
|
+
}, [texture, repeat, repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[0], repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[1], generateMipmaps, minFilter, magFilter]);
|
|
62
81
|
if (!properties) {
|
|
63
82
|
return _jsx("meshStandardMaterial", { color: "red", wireframe: true });
|
|
64
83
|
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { ModelListViewer } from '../../assetviewer/page';
|
|
3
3
|
import { useEffect, useState, useMemo } from 'react';
|
|
4
4
|
import { Label } from './Input';
|
|
5
|
-
function ModelComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
5
|
+
function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
|
|
6
6
|
const [modelFiles, setModelFiles] = useState([]);
|
|
7
7
|
useEffect(() => {
|
|
8
8
|
const base = basePath ? `${basePath}/` : '';
|
|
@@ -16,7 +16,7 @@ function ModelComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
|
16
16
|
const filename = file.startsWith('/') ? file.slice(1) : file;
|
|
17
17
|
onUpdate({ 'filename': filename });
|
|
18
18
|
};
|
|
19
|
-
return _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Model" }), _jsx("div", { style: { maxHeight: 128, overflowY: 'auto' }, children: _jsx(ModelListViewer, { files: modelFiles, selected: component.properties.filename ? `/${component.properties.filename}` : undefined, onSelect: handleModelSelect, basePath: basePath }) })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("input", { type: "checkbox", id: "instanced-checkbox", checked: component.properties.instanced || false, onChange: e => onUpdate({ instanced: e.target.checked }), style: { width: 12, height: 12 } }), _jsx("label", { htmlFor: "instanced-checkbox", style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Instanced" })] })] });
|
|
19
|
+
return _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Model" }), _jsx("div", { style: { maxHeight: 128, overflowY: 'auto' }, children: _jsx(ModelListViewer, { files: modelFiles, selected: component.properties.filename ? `/${component.properties.filename}` : undefined, onSelect: handleModelSelect, basePath: basePath }, node === null || node === void 0 ? void 0 : node.id) })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("input", { type: "checkbox", id: "instanced-checkbox", checked: component.properties.instanced || false, onChange: e => onUpdate({ instanced: e.target.checked }), style: { width: 12, height: 12 } }), _jsx("label", { htmlFor: "instanced-checkbox", style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Instanced" })] })] });
|
|
20
20
|
}
|
|
21
21
|
// View for Model component
|
|
22
22
|
function ModelComponentView({ properties, loadedModels, children }) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
|
|
2
|
+
class SoundManager {
|
|
3
|
+
private static _instance: SoundManager | null = null
|
|
4
|
+
|
|
5
|
+
public context: AudioContext
|
|
6
|
+
private buffers = new Map<string, AudioBuffer>()
|
|
7
|
+
|
|
8
|
+
private masterGain: GainNode
|
|
9
|
+
private sfxGain: GainNode
|
|
10
|
+
private musicGain: GainNode
|
|
11
|
+
|
|
12
|
+
private constructor() {
|
|
13
|
+
const AudioCtx =
|
|
14
|
+
window.AudioContext || (window as any).webkitAudioContext
|
|
15
|
+
|
|
16
|
+
this.context = new AudioCtx()
|
|
17
|
+
|
|
18
|
+
this.masterGain = this.context.createGain()
|
|
19
|
+
this.sfxGain = this.context.createGain()
|
|
20
|
+
this.musicGain = this.context.createGain()
|
|
21
|
+
|
|
22
|
+
this.sfxGain.connect(this.masterGain)
|
|
23
|
+
this.musicGain.connect(this.masterGain)
|
|
24
|
+
this.masterGain.connect(this.context.destination)
|
|
25
|
+
|
|
26
|
+
this.masterGain.gain.value = 1
|
|
27
|
+
this.sfxGain.gain.value = 1
|
|
28
|
+
this.musicGain.gain.value = 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Singleton accessor */
|
|
32
|
+
static get instance(): SoundManager {
|
|
33
|
+
if (typeof window === 'undefined') {
|
|
34
|
+
// Return a dummy instance for SSR
|
|
35
|
+
return new Proxy({} as SoundManager, {
|
|
36
|
+
get: () => () => {}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
if (!SoundManager._instance) {
|
|
40
|
+
SoundManager._instance = new SoundManager()
|
|
41
|
+
}
|
|
42
|
+
return SoundManager._instance
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Required once after user gesture (browser) */
|
|
46
|
+
resume() {
|
|
47
|
+
if (this.context.state !== "running") {
|
|
48
|
+
this.context.resume()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Preload a sound from URL */
|
|
53
|
+
async load(path: string, url: string) {
|
|
54
|
+
if (this.buffers.has(path)) return
|
|
55
|
+
|
|
56
|
+
const res = await fetch(url)
|
|
57
|
+
const arrayBuffer = await res.arrayBuffer()
|
|
58
|
+
const buffer = await this.context.decodeAudioData(arrayBuffer)
|
|
59
|
+
|
|
60
|
+
this.buffers.set(path, buffer)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Play from already-loaded buffer (fails silently if not loaded) */
|
|
64
|
+
playSync(
|
|
65
|
+
path: string,
|
|
66
|
+
{
|
|
67
|
+
volume = 1,
|
|
68
|
+
playbackRate = 1,
|
|
69
|
+
detune = 0,
|
|
70
|
+
pitch = 1,
|
|
71
|
+
}: {
|
|
72
|
+
volume?: number
|
|
73
|
+
playbackRate?: number
|
|
74
|
+
detune?: number
|
|
75
|
+
pitch?: number
|
|
76
|
+
} = {}
|
|
77
|
+
) {
|
|
78
|
+
this.resume()
|
|
79
|
+
|
|
80
|
+
const buffer = this.buffers.get(path)
|
|
81
|
+
if (!buffer) return
|
|
82
|
+
|
|
83
|
+
const src = this.context.createBufferSource()
|
|
84
|
+
const gain = this.context.createGain()
|
|
85
|
+
|
|
86
|
+
src.buffer = buffer
|
|
87
|
+
src.playbackRate.value = playbackRate * pitch
|
|
88
|
+
src.detune.value = detune
|
|
89
|
+
|
|
90
|
+
gain.gain.value = volume
|
|
91
|
+
|
|
92
|
+
src.connect(gain)
|
|
93
|
+
gain.connect(this.sfxGain)
|
|
94
|
+
|
|
95
|
+
src.start()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Load and play SFX - accepts file path directly */
|
|
99
|
+
async play(
|
|
100
|
+
path: string,
|
|
101
|
+
options?: {
|
|
102
|
+
volume?: number
|
|
103
|
+
playbackRate?: number
|
|
104
|
+
detune?: number
|
|
105
|
+
pitch?: number
|
|
106
|
+
}
|
|
107
|
+
) {
|
|
108
|
+
// Auto-load from path if not already loaded
|
|
109
|
+
if (!this.buffers.has(path)) {
|
|
110
|
+
await this.load(path, path)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.playSync(path, options)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Volume controls */
|
|
117
|
+
setMasterVolume(v: number) {
|
|
118
|
+
this.masterGain.gain.value = v
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setSfxVolume(v: number) {
|
|
122
|
+
this.sfxGain.gain.value = v
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setMusicVolume(v: number) {
|
|
126
|
+
this.musicGain.gain.value = v
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const sound = SoundManager.instance
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { Canvas, extend } from "@react-three/fiber";
|
|
3
|
+
import { Canvas, extend, CanvasProps } from "@react-three/fiber";
|
|
4
4
|
import { WebGPURenderer, MeshBasicNodeMaterial, MeshStandardNodeMaterial, SpriteNodeMaterial, PCFShadowMap } from "three/webgpu";
|
|
5
5
|
import { Suspense, useState } from "react";
|
|
6
6
|
import { WebGPURendererParameters } from "three/src/renderers/webgpu/WebGPURenderer.Nodes.js";
|
|
@@ -15,8 +15,13 @@ extend({
|
|
|
15
15
|
SpriteNodeMaterial: SpriteNodeMaterial,
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
interface GameCanvasProps extends Omit<CanvasProps, 'children'> {
|
|
19
|
+
loader?: boolean;
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
glConfig?: WebGPURendererParameters;
|
|
22
|
+
}
|
|
18
23
|
|
|
19
|
-
export default function GameCanvas({ loader = false, children, ...props }:
|
|
24
|
+
export default function GameCanvas({ loader = false, children, glConfig, ...props }: GameCanvasProps) {
|
|
20
25
|
const [frameloop, setFrameloop] = useState<"never" | "always">("never");
|
|
21
26
|
|
|
22
27
|
return <>
|
|
@@ -30,7 +35,7 @@ export default function GameCanvas({ loader = false, children, ...props }: { loa
|
|
|
30
35
|
// @ts-expect-error futuristic
|
|
31
36
|
shadowMap: true,
|
|
32
37
|
antialias: true,
|
|
33
|
-
...
|
|
38
|
+
...glConfig,
|
|
34
39
|
});
|
|
35
40
|
await renderer.init().then(() => {
|
|
36
41
|
setFrameloop("always");
|
|
@@ -40,6 +45,7 @@ export default function GameCanvas({ loader = false, children, ...props }: { loa
|
|
|
40
45
|
camera={{
|
|
41
46
|
position: [0, 1, 5],
|
|
42
47
|
}}
|
|
48
|
+
{...props}
|
|
43
49
|
>
|
|
44
50
|
<Suspense>
|
|
45
51
|
{children}
|
|
@@ -111,12 +111,19 @@ function NodeInspector({
|
|
|
111
111
|
}, [Object.keys(node.components || {}).join(',')]);
|
|
112
112
|
|
|
113
113
|
return <div style={inspector.content} className="prefab-scroll">
|
|
114
|
-
{/* Node
|
|
114
|
+
{/* Node Name */}
|
|
115
115
|
<div style={base.section}>
|
|
116
|
-
<div style={
|
|
116
|
+
<div style={{ display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }}>
|
|
117
|
+
<div style={{ fontSize: 10, color: '#888', wordBreak: 'break-all', border: '1px solid rgba(255,255,255,0.1)', padding: '2px 4px', borderRadius: 4, flex: 1 }}>
|
|
118
|
+
{node.id}
|
|
119
|
+
</div>
|
|
120
|
+
<button style={{ ...base.btn, ...base.btnDanger }} title="Delete Node" onClick={deleteNode}>❌</button>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
117
123
|
<input
|
|
118
124
|
style={base.input}
|
|
119
125
|
value={node.name ?? ""}
|
|
126
|
+
placeholder='Node name'
|
|
120
127
|
onChange={e =>
|
|
121
128
|
updateNode(n => ({ ...n, name: e.target.value }))
|
|
122
129
|
}
|
|
@@ -127,7 +134,6 @@ function NodeInspector({
|
|
|
127
134
|
<div style={base.section}>
|
|
128
135
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
|
129
136
|
<div style={base.label}>Components</div>
|
|
130
|
-
<button style={{ ...base.btn, ...base.btnDanger }} onClick={deleteNode}>Delete Node</button>
|
|
131
137
|
</div>
|
|
132
138
|
|
|
133
139
|
{node.components && Object.entries(node.components).map(([key, comp]: [string, any]) => {
|
|
@@ -143,6 +149,7 @@ function NodeInspector({
|
|
|
143
149
|
<div style={{ fontSize: 11, fontWeight: 500 }}>{key}</div>
|
|
144
150
|
<button
|
|
145
151
|
style={{ ...base.btn, padding: '2px 6px' }}
|
|
152
|
+
title="Remove Component"
|
|
146
153
|
onClick={() => updateNode(n => {
|
|
147
154
|
const { [key]: _, ...rest } = n.components || {};
|
|
148
155
|
return { ...n, components: rest };
|
|
@@ -154,6 +161,7 @@ function NodeInspector({
|
|
|
154
161
|
{def.Editor && (
|
|
155
162
|
<def.Editor
|
|
156
163
|
component={comp}
|
|
164
|
+
node={node}
|
|
157
165
|
onUpdate={(newProps: any) => updateNode(n => ({
|
|
158
166
|
...n,
|
|
159
167
|
components: {
|
|
@@ -174,7 +182,6 @@ function NodeInspector({
|
|
|
174
182
|
{/* Add Component */}
|
|
175
183
|
{available.length > 0 && (
|
|
176
184
|
<div>
|
|
177
|
-
<div style={base.label}>Add Component</div>
|
|
178
185
|
<div style={base.row}>
|
|
179
186
|
<select
|
|
180
187
|
style={{ ...base.input, flex: 1 }}
|
|
@@ -199,6 +206,7 @@ function NodeInspector({
|
|
|
199
206
|
}));
|
|
200
207
|
}
|
|
201
208
|
}}
|
|
209
|
+
title="Add Component"
|
|
202
210
|
>
|
|
203
211
|
+
|
|
204
212
|
</button>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { FC } from "react";
|
|
2
|
+
import { ComponentData, GameObject } from "../types";
|
|
2
3
|
|
|
3
4
|
export interface Component {
|
|
4
5
|
name: string;
|
|
5
6
|
Editor: FC<{
|
|
6
|
-
|
|
7
|
+
node?: GameObject;
|
|
8
|
+
component: ComponentData;
|
|
7
9
|
onUpdate: (newComp: any) => void;
|
|
8
10
|
basePath?: string;
|
|
9
11
|
transformMode?: "translate" | "rotate" | "scale";
|
|
@@ -2,6 +2,22 @@ import { TextureListViewer } from '../../assetviewer/page';
|
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { Component } from './ComponentRegistry';
|
|
4
4
|
import { Input, Label } from './Input';
|
|
5
|
+
import { useMemo } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
DoubleSide,
|
|
8
|
+
RepeatWrapping,
|
|
9
|
+
ClampToEdgeWrapping,
|
|
10
|
+
SRGBColorSpace,
|
|
11
|
+
Texture,
|
|
12
|
+
NearestFilter,
|
|
13
|
+
LinearFilter,
|
|
14
|
+
NearestMipmapNearestFilter,
|
|
15
|
+
NearestMipmapLinearFilter,
|
|
16
|
+
LinearMipmapNearestFilter,
|
|
17
|
+
LinearMipmapLinearFilter,
|
|
18
|
+
MinificationTextureFilter,
|
|
19
|
+
MagnificationTextureFilter
|
|
20
|
+
} from 'three';
|
|
5
21
|
|
|
6
22
|
function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { component: any; onUpdate: (newComp: any) => void; basePath?: string }) {
|
|
7
23
|
const [textureFiles, setTextureFiles] = useState<string[]>([]);
|
|
@@ -100,23 +116,76 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { compo
|
|
|
100
116
|
</div>
|
|
101
117
|
</div>
|
|
102
118
|
)}
|
|
119
|
+
|
|
120
|
+
<div style={{ marginTop: 4 }}>
|
|
121
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 }}>
|
|
122
|
+
<input
|
|
123
|
+
type="checkbox"
|
|
124
|
+
style={{ width: 12, height: 12 }}
|
|
125
|
+
checked={component.properties.generateMipmaps !== false}
|
|
126
|
+
onChange={e => onUpdate({ generateMipmaps: e.target.checked })}
|
|
127
|
+
/>
|
|
128
|
+
<label style={{ fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }}>Generate Mipmaps</label>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div>
|
|
132
|
+
<Label>Min Filter</Label>
|
|
133
|
+
<select
|
|
134
|
+
style={{ ...textInputStyle, width: '100%', cursor: 'pointer' }}
|
|
135
|
+
value={component.properties.minFilter || 'LinearMipmapLinearFilter'}
|
|
136
|
+
onChange={e => onUpdate({ minFilter: e.target.value })}
|
|
137
|
+
>
|
|
138
|
+
<option value="NearestFilter">Nearest</option>
|
|
139
|
+
<option value="NearestMipmapNearestFilter">Nearest Mipmap Nearest</option>
|
|
140
|
+
<option value="NearestMipmapLinearFilter">Nearest Mipmap Linear</option>
|
|
141
|
+
<option value="LinearFilter">Linear</option>
|
|
142
|
+
<option value="LinearMipmapNearestFilter">Linear Mipmap Nearest</option>
|
|
143
|
+
<option value="LinearMipmapLinearFilter">Linear Mipmap Linear (Default)</option>
|
|
144
|
+
</select>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div style={{ marginTop: 4 }}>
|
|
148
|
+
<Label>Mag Filter</Label>
|
|
149
|
+
<select
|
|
150
|
+
style={{ ...textInputStyle, width: '100%', cursor: 'pointer' }}
|
|
151
|
+
value={component.properties.magFilter || 'LinearFilter'}
|
|
152
|
+
onChange={e => onUpdate({ magFilter: e.target.value })}
|
|
153
|
+
>
|
|
154
|
+
<option value="NearestFilter">Nearest</option>
|
|
155
|
+
<option value="LinearFilter">Linear (Default)</option>
|
|
156
|
+
</select>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
103
159
|
</div>
|
|
104
160
|
)}
|
|
105
161
|
</div>
|
|
106
162
|
);
|
|
107
163
|
};
|
|
108
164
|
|
|
109
|
-
|
|
110
|
-
import { useMemo } from 'react';
|
|
111
|
-
import { DoubleSide, RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, Texture } from 'three';
|
|
112
|
-
|
|
113
165
|
// View for Material component
|
|
114
166
|
function MaterialComponentView({ properties, loadedTextures }: { properties: any, loadedTextures?: Record<string, Texture> }) {
|
|
115
167
|
const textureName = properties?.texture;
|
|
116
168
|
const repeat = properties?.repeat;
|
|
117
169
|
const repeatCount = properties?.repeatCount;
|
|
170
|
+
const generateMipmaps = properties?.generateMipmaps !== false;
|
|
171
|
+
const minFilter = properties?.minFilter || 'LinearMipmapLinearFilter';
|
|
172
|
+
const magFilter = properties?.magFilter || 'LinearFilter';
|
|
118
173
|
const texture = textureName && loadedTextures ? loadedTextures[textureName] : undefined;
|
|
119
174
|
|
|
175
|
+
const minFilterMap: Record<string, MinificationTextureFilter> = {
|
|
176
|
+
NearestFilter,
|
|
177
|
+
LinearFilter,
|
|
178
|
+
NearestMipmapNearestFilter,
|
|
179
|
+
NearestMipmapLinearFilter,
|
|
180
|
+
LinearMipmapNearestFilter,
|
|
181
|
+
LinearMipmapLinearFilter
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const magFilterMap: Record<string, MagnificationTextureFilter> = {
|
|
185
|
+
NearestFilter,
|
|
186
|
+
LinearFilter
|
|
187
|
+
};
|
|
188
|
+
|
|
120
189
|
const finalTexture = useMemo(() => {
|
|
121
190
|
if (!texture) return undefined;
|
|
122
191
|
const t = texture.clone();
|
|
@@ -128,9 +197,12 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: any
|
|
|
128
197
|
t.repeat.set(1, 1);
|
|
129
198
|
}
|
|
130
199
|
t.colorSpace = SRGBColorSpace;
|
|
200
|
+
t.generateMipmaps = generateMipmaps;
|
|
201
|
+
t.minFilter = minFilterMap[minFilter] ?? LinearMipmapLinearFilter;
|
|
202
|
+
t.magFilter = magFilterMap[magFilter] ?? LinearFilter;
|
|
131
203
|
t.needsUpdate = true;
|
|
132
204
|
return t;
|
|
133
|
-
}, [texture, repeat, repeatCount?.[0], repeatCount?.[1]]);
|
|
205
|
+
}, [texture, repeat, repeatCount?.[0], repeatCount?.[1], generateMipmaps, minFilter, magFilter]);
|
|
134
206
|
|
|
135
207
|
if (!properties) {
|
|
136
208
|
return <meshStandardMaterial color="red" wireframe />;
|
|
@@ -2,8 +2,9 @@ import { ModelListViewer } from '../../assetviewer/page';
|
|
|
2
2
|
import { useEffect, useState, useMemo } from 'react';
|
|
3
3
|
import { Component } from './ComponentRegistry';
|
|
4
4
|
import { Label } from './Input';
|
|
5
|
+
import { GameObject } from '../types';
|
|
5
6
|
|
|
6
|
-
function ModelComponentEditor({ component, onUpdate, basePath = "" }: { component: any; onUpdate: (newComp: any) => void; basePath?: string }) {
|
|
7
|
+
function ModelComponentEditor({ component, node, onUpdate, basePath = "" }: { component: any; node?: GameObject; onUpdate: (newComp: any) => void; basePath?: string }) {
|
|
7
8
|
const [modelFiles, setModelFiles] = useState<string[]>([]);
|
|
8
9
|
|
|
9
10
|
useEffect(() => {
|
|
@@ -25,6 +26,7 @@ function ModelComponentEditor({ component, onUpdate, basePath = "" }: { componen
|
|
|
25
26
|
<Label>Model</Label>
|
|
26
27
|
<div style={{ maxHeight: 128, overflowY: 'auto' }}>
|
|
27
28
|
<ModelListViewer
|
|
29
|
+
key={node?.id}
|
|
28
30
|
files={modelFiles}
|
|
29
31
|
selected={component.properties.filename ? `/${component.properties.filename}` : undefined}
|
|
30
32
|
onSelect={handleModelSelect}
|