react-three-game 0.0.95 → 0.0.96

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.
@@ -1,6 +1,6 @@
1
- import React from "react";
2
- import { ThreeEvent } from '@react-three/fiber';
3
- import { Object3D, Group } from "three";
1
+ import type { ReactNode } from "react";
2
+ import type { ThreeEvent } from '@react-three/fiber';
3
+ import type { Object3D } from "three";
4
4
  export type RepeatAxisConfig = {
5
5
  axis: 'x' | 'y' | 'z';
6
6
  count: number;
@@ -8,7 +8,7 @@ export type RepeatAxisConfig = {
8
8
  };
9
9
  export declare const DEFAULT_REPEAT_AXES: RepeatAxisConfig[];
10
10
  export declare function normalizeRepeatAxes(value: unknown): RepeatAxisConfig[];
11
- export declare function getRepeatAxesFromModelProperties(properties: Record<string, any>): RepeatAxisConfig[];
11
+ export declare function getRepeatAxesFromModelProperties(properties: Record<string, unknown>): RepeatAxisConfig[];
12
12
  export type InstanceData = {
13
13
  id: string;
14
14
  sourceId: string;
@@ -20,7 +20,7 @@ export type InstanceData = {
20
20
  meshPath: string;
21
21
  };
22
22
  export declare function GameInstanceProvider({ children, models, onSelect, onClick, registerRef, selectedId, editMode }: {
23
- children: React.ReactNode;
23
+ children: ReactNode;
24
24
  models: {
25
25
  [filename: string]: Object3D;
26
26
  };
@@ -31,7 +31,7 @@ export declare function GameInstanceProvider({ children, models, onSelect, onCli
31
31
  editMode?: boolean;
32
32
  }): import("react/jsx-runtime").JSX.Element;
33
33
  export declare function useInstanceCheck(id: string): boolean;
34
- export declare const GameInstance: React.ForwardRefExoticComponent<{
34
+ export declare function GameInstance({ id, sourceId, modelUrl, locked, position, rotation, scale, visible, onClick: _onClick, }: {
35
35
  id: string;
36
36
  sourceId?: string;
37
37
  modelUrl: string;
@@ -41,4 +41,4 @@ export declare const GameInstance: React.ForwardRefExoticComponent<{
41
41
  scale: [number, number, number];
42
42
  visible?: boolean;
43
43
  onClick?: (event: ThreeEvent<PointerEvent>, nodeId: string, object: Object3D | null) => void;
44
- } & React.RefAttributes<Group<import("three").Object3DEventMap>>>;
44
+ }): null;
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React, { createContext, useContext, useMemo, useRef, useState, useEffect } from "react";
2
+ import { createContext, useContext, useMemo, useRef, useState, useEffect } from "react";
3
3
  import { Merged, useHelper } from '@react-three/drei';
4
4
  import { Mesh, Matrix4, BoxHelper } from "three";
5
5
  import { useStore } from "zustand";
6
6
  import { createStore } from "zustand/vanilla";
7
7
  import { usePointerEvents } from "./usePointerEvents";
8
8
  export const DEFAULT_REPEAT_AXES = [{ axis: 'x', count: 1, offset: 1 }];
9
+ const EMPTY_INSTANCE_STORE = createInstanceRegistryStore();
9
10
  export function normalizeRepeatAxes(value) {
10
11
  if (!Array.isArray(value)) {
11
12
  return DEFAULT_REPEAT_AXES;
@@ -14,14 +15,15 @@ export function normalizeRepeatAxes(value) {
14
15
  const normalized = value.reduce((result, entry) => {
15
16
  if (!entry || typeof entry !== 'object')
16
17
  return result;
17
- const axisValue = entry.axis;
18
+ const record = entry;
19
+ const axisValue = record.axis;
18
20
  if (axisValue !== 'x' && axisValue !== 'y' && axisValue !== 'z')
19
21
  return result;
20
22
  if (seen.has(axisValue))
21
23
  return result;
22
24
  seen.add(axisValue);
23
- const countValue = Number(entry.count);
24
- const offsetValue = Number(entry.offset);
25
+ const countValue = Number(record.count);
26
+ const offsetValue = Number(record.offset);
25
27
  result.push({
26
28
  axis: axisValue,
27
29
  count: Number.isFinite(countValue) ? Math.max(1, Math.floor(countValue)) : 1,
@@ -141,7 +143,7 @@ export function GameInstanceProvider({ children, models, onSelect, onClick, regi
141
143
  const rootInverse = new Matrix4().copy(model.matrixWorld).invert();
142
144
  let partIndex = 0;
143
145
  model.traverse((obj) => {
144
- if (obj.isMesh) {
146
+ if (obj instanceof Mesh) {
145
147
  // Clone geometry and bake relative transform
146
148
  const geom = obj.geometry.clone();
147
149
  geom.applyMatrix4(obj.matrixWorld.clone().premultiply(rootInverse));
@@ -157,7 +159,9 @@ export function GameInstanceProvider({ children, models, onSelect, onClick, regi
157
159
  // Cleanup geometries when models change
158
160
  useEffect(() => {
159
161
  return () => {
160
- Object.values(flatMeshes).forEach(mesh => mesh.geometry.dispose());
162
+ Object.values(flatMeshes).forEach(mesh => {
163
+ mesh.geometry.dispose();
164
+ });
161
165
  };
162
166
  }, [flatMeshes]);
163
167
  const instances = useMemo(() => Object.values(instancesById), [instancesById]);
@@ -194,12 +198,16 @@ export function GameInstanceProvider({ children, models, onSelect, onClick, regi
194
198
  })] }));
195
199
  }
196
200
  function InstancedGroup({ modelKey, group, partCount, instancesMap, onSelect, onClick, registerRef, selectedId, editMode }) {
197
- const InstanceComponents = useMemo(() => Array.from({ length: partCount }, (_, i) => instancesMap[`${modelKey}__${i}`]).filter(Boolean), [instancesMap, modelKey, partCount]);
201
+ const instanceEntries = useMemo(() => Array.from({ length: partCount }, (_, i) => {
202
+ const partKey = `${modelKey}__${i}`;
203
+ const Component = instancesMap[partKey];
204
+ return Component ? { partKey, Component } : null;
205
+ }).filter((entry) => Boolean(entry)), [instancesMap, modelKey, partCount]);
198
206
  const visibleInstances = useMemo(() => group.instances.filter(instance => instance.visible !== false), [group.instances]);
199
- return (_jsx(_Fragment, { children: visibleInstances.map(inst => (_jsx(InstanceGroupItem, { instance: inst, InstanceComponents: InstanceComponents, onSelect: onSelect, onClick: onClick, registerRef: registerRef, selectedId: selectedId, editMode: editMode }, inst.id))) }));
207
+ return (_jsx(_Fragment, { children: visibleInstances.map(inst => (_jsx(InstanceGroupItem, { instance: inst, instanceEntries: instanceEntries, onSelect: onSelect, onClick: onClick, registerRef: registerRef, selectedId: selectedId, editMode: editMode }, inst.id))) }));
200
208
  }
201
209
  // Individual instance item with its own click state
202
- function InstanceGroupItem({ instance, InstanceComponents, onSelect, onClick, registerRef, selectedId, editMode }) {
210
+ function InstanceGroupItem({ instance, instanceEntries, onSelect, onClick, registerRef, selectedId, editMode }) {
203
211
  const groupRef = useRef(null);
204
212
  const isLocked = Boolean(instance.locked);
205
213
  const isSelected = selectedId === instance.id || selectedId === instance.sourceId;
@@ -217,24 +225,26 @@ function InstanceGroupItem({ instance, InstanceComponents, onSelect, onClick, re
217
225
  },
218
226
  });
219
227
  // Use BoxHelper when object is selected in edit mode
220
- useHelper(editMode && isSelected ? groupRef : null, BoxHelper, 'cyan');
228
+ const helperTarget = editMode && isSelected && groupRef.current
229
+ ? { current: groupRef.current }
230
+ : null;
231
+ useHelper(helperTarget, BoxHelper, 'cyan');
221
232
  useEffect(() => {
222
233
  if (editMode)
223
234
  return;
224
235
  registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, groupRef.current);
225
236
  return () => registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, null);
226
237
  }, [editMode, instance.id, registerRef]);
227
- return (_jsx("group", Object.assign({ ref: groupRef, position: instance.position, rotation: instance.rotation, scale: instance.scale }, pointerHandlers, { children: InstanceComponents.map((Instance, i) => _jsx(Instance, {}, i)) })));
238
+ return (_jsx("group", Object.assign({ ref: groupRef, position: instance.position, rotation: instance.rotation, scale: instance.scale }, pointerHandlers, { children: instanceEntries.map(({ partKey, Component }) => _jsx(Component, {}, partKey)) })));
228
239
  }
229
240
  export function useInstanceCheck(id) {
241
+ var _a;
230
242
  const ctx = useContext(GameInstanceContext);
231
- return ctx ? useStore(ctx.store, state => Boolean(state.instancesById[id] || state.sourceInstanceIdsById[id])) : false;
243
+ const store = (_a = ctx === null || ctx === void 0 ? void 0 : ctx.store) !== null && _a !== void 0 ? _a : EMPTY_INSTANCE_STORE;
244
+ return useStore(store, state => Boolean(state.instancesById[id] || state.sourceInstanceIdsById[id]));
232
245
  }
233
- export const GameInstance = React.forwardRef(({ id, sourceId, modelUrl, locked = false, position, rotation, scale, visible = true, onClick: _onClick, }, ref) => {
246
+ export function GameInstance({ id, sourceId, modelUrl, locked = false, position, rotation, scale, visible = true, onClick: _onClick, }) {
234
247
  const ctx = useContext(GameInstanceContext);
235
- const [positionX, positionY, positionZ] = position;
236
- const [rotationX, rotationY, rotationZ] = rotation;
237
- const [scaleX, scaleY, scaleZ] = scale;
238
248
  const instance = useMemo(() => ({
239
249
  id,
240
250
  sourceId: sourceId !== null && sourceId !== void 0 ? sourceId : id,
@@ -244,22 +254,7 @@ export const GameInstance = React.forwardRef(({ id, sourceId, modelUrl, locked =
244
254
  position,
245
255
  rotation,
246
256
  scale,
247
- }), [
248
- id,
249
- sourceId,
250
- locked,
251
- visible,
252
- modelUrl,
253
- positionX,
254
- positionY,
255
- positionZ,
256
- rotationX,
257
- rotationY,
258
- rotationZ,
259
- scaleX,
260
- scaleY,
261
- scaleZ,
262
- ]);
257
+ }), [id, sourceId, locked, visible, modelUrl, position, rotation, scale]);
263
258
  useEffect(() => {
264
259
  if (!ctx)
265
260
  return;
@@ -269,6 +264,6 @@ export const GameInstance = React.forwardRef(({ id, sourceId, modelUrl, locked =
269
264
  return () => {
270
265
  removeInstance(instance.id);
271
266
  };
272
- }, [ctx === null || ctx === void 0 ? void 0 : ctx.store, instance]);
267
+ }, [ctx, instance]);
273
268
  return null;
274
- });
269
+ }
@@ -1,5 +1,5 @@
1
1
  import GameCanvas from "../../shared/GameCanvas";
2
- import { Prefab } from "./types";
2
+ import type { Prefab } from "./types";
3
3
  import { PrefabEditorMode, type Scene } from "./PrefabRoot";
4
4
  import type { ExportGLBOptions } from "./utils";
5
5
  export interface PrefabEditorRef extends Scene {
@@ -79,6 +79,10 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
79
79
  const transformControlsRef = useRef(null);
80
80
  const onChangeRef = useRef(onChange);
81
81
  const isEditMode = mode === PrefabEditorMode.Edit;
82
+ const detachTransformControls = useCallback(() => {
83
+ var _a;
84
+ (_a = transformControlsRef.current) === null || _a === void 0 ? void 0 : _a.detach();
85
+ }, []);
82
86
  const getPrefab = useCallback(() => denormalizePrefab(prefabStore.getState()), [prefabStore]);
83
87
  const getNode = useCallback((nodeId) => { var _a; return (_a = prefabStore.getState().nodesById[nodeId]) !== null && _a !== void 0 ? _a : null; }, [prefabStore]);
84
88
  const getRoot = useCallback(() => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root) !== null && _b !== void 0 ? _b : null; }, []);
@@ -158,13 +162,13 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
158
162
  updateMode(initialMode);
159
163
  }, [initialMode, updateMode]);
160
164
  const loadPrefab = useCallback((prefab, options) => {
161
- var _a, _b;
162
- (_a = transformControlsRef.current) === null || _a === void 0 ? void 0 : _a.detach();
165
+ var _a;
166
+ detachTransformControls();
163
167
  const before = prefabStore.getState();
164
168
  prefabStore.getState().replacePrefab(prefab);
165
169
  const after = prefabStore.getState();
166
170
  if (after !== before && (options === null || options === void 0 ? void 0 : options.notifyChange) !== false) {
167
- (_b = onChangeRef.current) === null || _b === void 0 ? void 0 : _b.call(onChangeRef, prefab);
171
+ (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, prefab);
168
172
  }
169
173
  if (options === null || options === void 0 ? void 0 : options.resetHistory) {
170
174
  setSelectedId(null);
@@ -175,7 +179,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
175
179
  else {
176
180
  setSelectedId(prev => prev && prefabStore.getState().nodesById[prev] ? prev : null);
177
181
  }
178
- }, [prefabStore]);
182
+ }, [detachTransformControls, prefabStore]);
179
183
  useEffect(() => {
180
184
  if (initialPrefab)
181
185
  loadPrefab(initialPrefab, { resetHistory: true, notifyChange: false });
@@ -205,17 +209,25 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
205
209
  const importPrefab = useCallback((prefab) => {
206
210
  add(regenerateIds(prefab.root));
207
211
  }, [add]);
208
- const applyHistory = (index) => {
209
- var _a, _b;
210
- (_a = transformControlsRef.current) === null || _a === void 0 ? void 0 : _a.detach();
212
+ const applyHistory = useCallback((index) => {
213
+ var _a;
214
+ detachTransformControls();
211
215
  prefabStore.getState().replacePrefab(history[index]);
212
- (_b = onChangeRef.current) === null || _b === void 0 ? void 0 : _b.call(onChangeRef, history[index]);
216
+ (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, history[index]);
213
217
  historyIndexRef.current = index;
214
218
  setHistoryIndex(index);
215
219
  setSelectedId(prev => prev && prefabStore.getState().nodesById[prev] ? prev : null);
216
- };
217
- const undo = () => historyIndex > 0 && applyHistory(historyIndex - 1);
218
- const redo = () => historyIndex < history.length - 1 && applyHistory(historyIndex + 1);
220
+ }, [detachTransformControls, history, prefabStore]);
221
+ const undo = useCallback(() => {
222
+ if (historyIndex > 0) {
223
+ applyHistory(historyIndex - 1);
224
+ }
225
+ }, [applyHistory, historyIndex]);
226
+ const redo = useCallback(() => {
227
+ if (historyIndex < history.length - 1) {
228
+ applyHistory(historyIndex + 1);
229
+ }
230
+ }, [applyHistory, history.length, historyIndex]);
219
231
  useEffect(() => {
220
232
  if (!isEditMode)
221
233
  return;
@@ -233,7 +245,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
233
245
  };
234
246
  window.addEventListener('keydown', handleKeyDown);
235
247
  return () => window.removeEventListener('keydown', handleKeyDown);
236
- }, [isEditMode, historyIndex, history]);
248
+ }, [isEditMode, redo, undo]);
237
249
  const handleScreenshot = useCallback(() => {
238
250
  const canvas = canvasRef.current;
239
251
  if (!canvas)
@@ -383,7 +395,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
383
395
  }
384
396
  (_d = canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed) === null || _d === void 0 ? void 0 : _d.call(canvasProps, event);
385
397
  }
386
- : canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed, children: [content, isEditMode ? _jsx(SelectionHelper, { object: transformObject }) : null, isEditMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { ref: controlsRef, enableDamping: false, makeDefault: true }), transformObject && (_jsx(TransformControls, { ref: transformControlsRef, object: transformObject, mode: transformMode, space: "local", onObjectChange: handleTransformChange, translationSnap: positionSnap > 0 ? positionSnap : undefined, rotationSnap: rotationSnap > 0 ? rotationSnap : undefined, scaleSnap: scaleSnap > 0 ? scaleSnap : undefined }, `transform-${selectedId}-${transformMode}-${positionSnap}-${rotationSnap}-${scaleSnap}`))] }))] })), showUI && (_jsxs(_Fragment, { children: [_jsxs("div", { style: toolbar.panel, children: [_jsx("button", { style: base.btn, onClick: toggleMode, children: isEditMode ? "▶" : "⏸" }), uiPlugins] }), isEditMode && (_jsx(EditorUI, { selectedId: selectedId, setSelectedId: setSelection, getPrefab: getPrefab, onReplacePrefab: (prefab) => loadPrefab(prefab, { resetHistory: true }), onImportPrefab: importPrefab, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 }))] }))] }) }) });
398
+ : canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed, children: [content, isEditMode ? _jsx(SelectionHelper, { object: transformObject }) : null, isEditMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { ref: controlsRef, enableDamping: false, makeDefault: true }), transformObject && (_jsx(TransformControls, { ref: transformControlsRef, object: transformObject, mode: transformMode, space: transformMode === "translate" ? "world" : "local", onObjectChange: handleTransformChange, translationSnap: positionSnap > 0 ? positionSnap : undefined, rotationSnap: rotationSnap > 0 ? rotationSnap : undefined, scaleSnap: scaleSnap > 0 ? scaleSnap : undefined }, `transform-${selectedId}-${transformMode}-${positionSnap}-${rotationSnap}-${scaleSnap}`))] }))] })), showUI && (_jsxs(_Fragment, { children: [_jsxs("div", { style: toolbar.panel, children: [_jsx("button", { type: "button", style: base.btn, onClick: toggleMode, children: isEditMode ? "▶" : "⏸" }), uiPlugins] }), isEditMode && (_jsx(EditorUI, { selectedId: selectedId, setSelectedId: setSelection, getPrefab: getPrefab, onReplacePrefab: (prefab) => loadPrefab(prefab, { resetHistory: true }), onImportPrefab: importPrefab, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 }))] }))] }) }) });
387
399
  });
388
400
  PrefabEditor.displayName = "PrefabEditor";
389
401
  export default PrefabEditor;
@@ -1,8 +1,9 @@
1
- import { Matrix4, Object3D, Texture } from "three";
2
- import { ThreeEvent } from "@react-three/fiber";
3
- import { GameObject as GameObjectType, Prefab } from "./types";
4
- import { LoadedModels } from "../dragdrop";
5
- import { PrefabStoreApi } from "./prefabStore";
1
+ import { Matrix4 } from "three";
2
+ import type { Object3D, Texture } from "three";
3
+ import type { ThreeEvent } from "@react-three/fiber";
4
+ import type { GameObject as GameObjectType, Prefab } from "./types";
5
+ import type { LoadedModels } from "../dragdrop";
6
+ import type { PrefabStoreApi } from "./prefabStore";
6
7
  export declare enum PrefabEditorMode {
7
8
  Edit = "edit",
8
9
  Play = "play"
@@ -11,14 +11,14 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
13
13
  import { createContext, forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
14
- import { Euler, Matrix4, } from "three";
14
+ import { Euler, Matrix4 } from "three";
15
15
  import { useStore } from "zustand";
16
16
  import { useClickValid } from "./useClickValid";
17
17
  import { findComponent, getNodeUserData } from "./types";
18
18
  import { getComponentDef, getComponentAssetRefs, registerComponent } from "./components/ComponentRegistry";
19
19
  import { builtinComponents } from "./components";
20
20
  import { loadModel, loadSound, loadTexture } from "../dragdrop";
21
- import { GameInstance, GameInstanceProvider, getRepeatAxesFromModelProperties, useInstanceCheck } from "./InstanceProvider";
21
+ import { GameInstance, GameInstanceProvider, getRepeatAxesFromModelProperties } from "./InstanceProvider";
22
22
  import { composeTransform, decompose } from "./utils";
23
23
  import { createPrefabStore, PrefabStoreProvider, usePrefabChildIds, usePrefabNode, usePrefabRootId } from "./prefabStore";
24
24
  import { AssetRuntimeContext, NodeScope } from "./assetRuntime";
@@ -29,6 +29,14 @@ const IDENTITY = new Matrix4();
29
29
  const EMPTY_MODELS = {};
30
30
  const EMPTY_TEXTURES = {};
31
31
  const EMPTY_SOUNDS = {};
32
+ const EMPTY_NODE_COMPONENTS = {
33
+ geometry: undefined,
34
+ material: undefined,
35
+ model: undefined,
36
+ sprite: undefined,
37
+ clickEventName: null,
38
+ composition: [],
39
+ };
32
40
  /** Resolve a relative or absolute asset file path against a base path. */
33
41
  function resolveAssetPath(basePath, file) {
34
42
  if (file.startsWith("http://") || file.startsWith("https://"))
@@ -84,6 +92,9 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
84
92
  throw new Error("PrefabRoot requires either a `data` or `store` prop");
85
93
  });
86
94
  const resolvedStore = store !== null && store !== void 0 ? store : ownedStore;
95
+ if (!resolvedStore) {
96
+ throw new Error("PrefabRoot requires either a `data` or `store` prop");
97
+ }
87
98
  const usesOwnedStore = resolvedStore === ownedStore;
88
99
  const rootId = useStore(resolvedStore, state => state.rootId);
89
100
  const assetManifestKey = useStore(resolvedStore, state => state.assetManifestKey);
@@ -155,6 +166,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
155
166
  }
156
167
  }, [data, resolvedStore, usesOwnedStore]);
157
168
  useEffect(() => {
169
+ void assetManifestKey;
158
170
  const syncAssets = (snapshot = resolvedStore.getState()) => {
159
171
  const modelsToLoad = new Set();
160
172
  const texturesToLoad = new Set();
@@ -187,23 +199,34 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
187
199
  }
188
200
  });
189
201
  };
190
- modelsToLoad.forEach(file => loadAsset(file, models, injectedModels, failedModels.current, path => loadModel(path).then(result => {
191
- if (result.success && result.model)
192
- setModels(m => (Object.assign(Object.assign({}, m), { [file]: result.model })));
193
- return result;
194
- })));
195
- texturesToLoad.forEach(file => loadAsset(file, textures, injectedTextures, failedTextures.current, path => loadTexture(path).then(result => {
196
- if (result.success && result.texture)
197
- setTextures(t => (Object.assign(Object.assign({}, t), { [file]: result.texture })));
198
- return result;
199
- })));
200
- soundsToLoad.forEach(file => loadAsset(file, sounds, injectedSounds, failedSounds.current, path => loadSound(path).then(result => {
201
- if (result.success && result.sound) {
202
- soundManager.setBuffer(file, result.sound);
203
- setSounds(s => (Object.assign(Object.assign({}, s), { [file]: result.sound })));
204
- }
205
- return result;
206
- })));
202
+ modelsToLoad.forEach(file => {
203
+ loadAsset(file, models, injectedModels, failedModels.current, path => loadModel(path).then(result => {
204
+ const loadedModel = result.model;
205
+ if (result.success && loadedModel) {
206
+ setModels(currentModels => (Object.assign(Object.assign({}, currentModels), { [file]: loadedModel })));
207
+ }
208
+ return result;
209
+ }));
210
+ });
211
+ texturesToLoad.forEach(file => {
212
+ loadAsset(file, textures, injectedTextures, failedTextures.current, path => loadTexture(path).then(result => {
213
+ const loadedTexture = result.texture;
214
+ if (result.success && loadedTexture) {
215
+ setTextures(currentTextures => (Object.assign(Object.assign({}, currentTextures), { [file]: loadedTexture })));
216
+ }
217
+ return result;
218
+ }));
219
+ });
220
+ soundsToLoad.forEach(file => {
221
+ loadAsset(file, sounds, injectedSounds, failedSounds.current, path => loadSound(path).then(result => {
222
+ const loadedSound = result.sound;
223
+ if (result.success && loadedSound) {
224
+ soundManager.setBuffer(file, loadedSound);
225
+ setSounds(currentSounds => (Object.assign(Object.assign({}, currentSounds), { [file]: loadedSound })));
226
+ }
227
+ return result;
228
+ }));
229
+ });
207
230
  };
208
231
  syncAssets();
209
232
  }, [resolvedStore, assetManifestKey, basePath, injectedModels, injectedSounds, injectedTextures, models, sounds, textures]);
@@ -335,13 +358,11 @@ export function GameObjectRenderer(props) {
335
358
  function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef, onSelect, onEditNodeClick, onClick, isVisible = true }) {
336
359
  var _a, _b;
337
360
  const gameObject = usePrefabNode(nodeId);
338
- if (!gameObject)
339
- return null;
340
- const analyzedComponents = useMemo(() => analyzeNodeComponents(gameObject), [gameObject]);
361
+ const analyzedComponents = useMemo(() => gameObject ? analyzeNodeComponents(gameObject) : EMPTY_NODE_COMPONENTS, [gameObject]);
341
362
  const localTransform = getNodeTransformProps(gameObject);
342
- const isLocked = Boolean(gameObject.locked);
343
- const nodeVisible = isVisible && !gameObject.hidden;
344
- const metadataProps = getNodeMetadataProps(gameObject);
363
+ const isLocked = Boolean(gameObject === null || gameObject === void 0 ? void 0 : gameObject.locked);
364
+ const nodeVisible = isVisible && !(gameObject === null || gameObject === void 0 ? void 0 : gameObject.hidden);
365
+ const metadataProps = gameObject ? getNodeMetadataProps(gameObject) : { name: '', userData: {} };
345
366
  const groupProps = Object.assign(Object.assign({}, metadataProps), { visible: nodeVisible, position: localTransform.position, rotation: localTransform.rotation, scale: localTransform.scale });
346
367
  const modelUrl = (_b = (_a = analyzedComponents.model) === null || _a === void 0 ? void 0 : _a.properties) === null || _b === void 0 ? void 0 : _b.filename;
347
368
  const instances = useMemo(() => buildRepeatedInstances(gameObject, parentMatrix, modelUrl), [gameObject, modelUrl, parentMatrix]);
@@ -353,9 +374,13 @@ function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef,
353
374
  }
354
375
  }, [editMode, nodeId, registerRef]);
355
376
  const editClickHandlers = useClickValid(!!editMode && !isLocked, (event) => {
377
+ if (!gameObject)
378
+ return;
356
379
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(nodeId);
357
380
  onEditNodeClick === null || onEditNodeClick === void 0 ? void 0 : onEditNodeClick(event, gameObject);
358
381
  });
382
+ if (!gameObject)
383
+ return null;
359
384
  const renderedInstances = instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, visible: nodeVisible, locked: isLocked, onClick: onClick }, instance.id)));
360
385
  if (editMode) {
361
386
  return (_jsxs(_Fragment, { children: [_jsx("group", Object.assign({ ref: handleGroupRef }, groupProps, editClickHandlers, { children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) })), renderedInstances] }));
@@ -365,20 +390,19 @@ function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef,
365
390
  function StandardNode({ nodeId, selectedId, onSelect, onClick, onEditNodeClick, registerRef, loadedModels, editMode, parentMatrix = IDENTITY, isVisible = true, }) {
366
391
  const gameObject = usePrefabNode(nodeId);
367
392
  const childIds = usePrefabChildIds(nodeId);
368
- if (!gameObject)
369
- return null;
370
- const analyzedComponents = useMemo(() => analyzeNodeComponents(gameObject), [gameObject]);
393
+ const analyzedComponents = useMemo(() => gameObject ? analyzeNodeComponents(gameObject) : EMPTY_NODE_COMPONENTS, [gameObject]);
371
394
  const isSelected = selectedId === nodeId;
372
- const isLocked = Boolean(gameObject.locked);
373
- const nodeVisible = isVisible && !gameObject.hidden;
374
- const stillInstanced = useInstanceCheck(nodeId);
375
- const metadataProps = getNodeMetadataProps(gameObject);
395
+ const isLocked = Boolean(gameObject === null || gameObject === void 0 ? void 0 : gameObject.locked);
396
+ const nodeVisible = isVisible && !(gameObject === null || gameObject === void 0 ? void 0 : gameObject.hidden);
397
+ const metadataProps = gameObject ? getNodeMetadataProps(gameObject) : { name: '', userData: {} };
376
398
  const groupRef = useRef(null);
377
399
  const handleGroupRef = useCallback((object) => {
378
400
  groupRef.current = object;
379
401
  registerRef(nodeId, object);
380
402
  }, [nodeId, registerRef]);
381
403
  const editClickHandlers = useClickValid(!!editMode && !isLocked, (event) => {
404
+ if (!gameObject)
405
+ return;
382
406
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(nodeId);
383
407
  onEditNodeClick === null || onEditNodeClick === void 0 ? void 0 : onEditNodeClick(event, gameObject);
384
408
  });
@@ -391,7 +415,6 @@ function StandardNode({ nodeId, selectedId, onSelect, onClick, onEditNodeClick,
391
415
  }
392
416
  : undefined;
393
417
  const world = parentMatrix.clone().multiply(compose(gameObject));
394
- const ready = isNodeReady(analyzedComponents.model, loadedModels);
395
418
  const transform = getNodeTransformProps(gameObject);
396
419
  const transformProps = {
397
420
  position: transform.position,
@@ -400,6 +423,8 @@ function StandardNode({ nodeId, selectedId, onSelect, onClick, onEditNodeClick,
400
423
  };
401
424
  const groupProps = Object.assign(Object.assign({}, metadataProps), transformProps);
402
425
  const childNodes = _jsx(ChildNodes, { childIds: childIds, parentMatrix: world, selectedId: selectedId, onSelect: onSelect, onClick: onClick, onEditNodeClick: onEditNodeClick, registerRef: registerRef, loadedModels: loadedModels, editMode: editMode, isVisible: nodeVisible });
426
+ if (!gameObject)
427
+ return null;
403
428
  const inner = renderNodeContent(analyzedComponents, loadedModels, primaryClickHandlers, childNodes);
404
429
  const editAnchor = editMode ? (_jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) })) : null;
405
430
  const standardNode = (_jsxs("group", Object.assign({ ref: handleGroupRef }, groupProps, { visible: nodeVisible }, (editMode ? editClickHandlers : undefined), { children: [editAnchor, inner] })));
@@ -422,7 +447,7 @@ function getModelRepeatSettings(node) {
422
447
  };
423
448
  }
424
449
  function buildRepeatedInstances(gameObject, parentMatrix, modelUrl) {
425
- if (!modelUrl)
450
+ if (!gameObject || !modelUrl)
426
451
  return [];
427
452
  const transform = getNodeTransformProps(gameObject);
428
453
  const repeat = getModelRepeatSettings(gameObject);
@@ -1,8 +1,8 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { ThreeElement } from '@react-three/fiber';
3
- import { Component } from './ComponentRegistry';
3
+ import type { Component } from './ComponentRegistry';
4
4
  import { MeshBasicNodeMaterial, MeshStandardNodeMaterial, SpriteNodeMaterial } from 'three/webgpu';
5
- import { MeshBasicMaterialProperties, MeshStandardMaterialProperties } from 'three';
5
+ import type { MeshBasicMaterialProperties, MeshStandardMaterialProperties } from 'three';
6
6
  declare module '@react-three/fiber' {
7
7
  interface ThreeElements {
8
8
  meshBasicNodeMaterial: ThreeElement<typeof MeshBasicNodeMaterial>;
@@ -10,7 +10,7 @@ declare module '@react-three/fiber' {
10
10
  spriteNodeMaterial: ThreeElement<typeof SpriteNodeMaterial>;
11
11
  }
12
12
  }
13
- export interface MaterialProps extends Omit<MeshStandardMaterialProperties & MeshBasicMaterialProperties, 'args' | 'normalScale'> {
13
+ export interface MaterialProps extends Omit<MeshStandardMaterialProperties & MeshBasicMaterialProperties, 'args' | 'normalScale' | 'side'> {
14
14
  materialType?: 'standard' | 'basic' | 'sprite';
15
15
  transmission?: number;
16
16
  thickness?: number;
@@ -28,8 +28,14 @@ export interface MaterialProps extends Omit<MeshStandardMaterialProperties & Mes
28
28
  magFilter?: string;
29
29
  normalMapTexture?: string;
30
30
  normalScale?: [number, number];
31
+ side?: keyof typeof SIDE_MAP;
31
32
  }
32
33
  export type MaterialOverrides = Record<string, unknown>;
34
+ declare const SIDE_MAP: {
35
+ readonly FrontSide: 0;
36
+ readonly BackSide: 1;
37
+ readonly DoubleSide: 2;
38
+ };
33
39
  export declare function useMaterialOverrides(): MaterialOverrides;
34
40
  export declare function MaterialOverridesProvider({ overrides, children, }: {
35
41
  overrides: MaterialOverrides;
@@ -24,6 +24,39 @@ function Vector2Editor({ label, value, onChange, min, max, step, }) {
24
24
  }
25
25
  const EMPTY_MATERIAL_OVERRIDES = Object.freeze({});
26
26
  const MaterialOverridesContext = createContext(EMPTY_MATERIAL_OVERRIDES);
27
+ const SIDE_MAP = { FrontSide, BackSide, DoubleSide };
28
+ const MIN_FILTER_MAP = {
29
+ NearestFilter,
30
+ LinearFilter,
31
+ NearestMipmapNearestFilter,
32
+ NearestMipmapLinearFilter,
33
+ LinearMipmapNearestFilter,
34
+ LinearMipmapLinearFilter,
35
+ };
36
+ const MAG_FILTER_MAP = {
37
+ NearestFilter,
38
+ LinearFilter,
39
+ };
40
+ function cloneConfiguredTexture({ texture, repeat, repeatCount, offset, colorSpace, generateMipmaps, minFilter, magFilter, }) {
41
+ var _a, _b;
42
+ const clonedTexture = texture.clone();
43
+ if (repeat) {
44
+ clonedTexture.wrapS = clonedTexture.wrapT = RepeatWrapping;
45
+ if (repeatCount)
46
+ clonedTexture.repeat.set(repeatCount[0], repeatCount[1]);
47
+ }
48
+ else {
49
+ clonedTexture.wrapS = clonedTexture.wrapT = ClampToEdgeWrapping;
50
+ clonedTexture.repeat.set(1, 1);
51
+ }
52
+ clonedTexture.offset.set((_a = offset === null || offset === void 0 ? void 0 : offset[0]) !== null && _a !== void 0 ? _a : 0, (_b = offset === null || offset === void 0 ? void 0 : offset[1]) !== null && _b !== void 0 ? _b : 0);
53
+ clonedTexture.colorSpace = colorSpace;
54
+ clonedTexture.generateMipmaps = generateMipmaps;
55
+ clonedTexture.minFilter = minFilter;
56
+ clonedTexture.magFilter = magFilter;
57
+ clonedTexture.needsUpdate = true;
58
+ return clonedTexture;
59
+ }
27
60
  export function useMaterialOverrides() {
28
61
  return useContext(MaterialOverridesContext);
29
62
  }
@@ -37,7 +70,7 @@ extend({
37
70
  MeshStandardNodeMaterial,
38
71
  SpriteNodeMaterial,
39
72
  });
40
- function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
73
+ function MaterialComponentEditor({ component, onUpdate, basePath = "", }) {
41
74
  var _a;
42
75
  const materialType = (_a = component.properties.materialType) !== null && _a !== void 0 ? _a : 'standard';
43
76
  const hasTexture = !!component.properties.texture;
@@ -155,94 +188,83 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
155
188
  }
156
189
  // View for Material component
157
190
  function MaterialComponentView({ properties: rawProps }) {
158
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
191
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
159
192
  const { getTexture } = useAssetRuntime();
160
193
  const properties = rawProps;
161
- const materialType = (_a = properties === null || properties === void 0 ? void 0 : properties.materialType) !== null && _a !== void 0 ? _a : 'standard';
162
- const textureName = properties === null || properties === void 0 ? void 0 : properties.texture;
163
- const offset = properties === null || properties === void 0 ? void 0 : properties.offset;
164
- const repeat = properties === null || properties === void 0 ? void 0 : properties.repeat;
165
- const repeatCount = properties === null || properties === void 0 ? void 0 : properties.repeatCount;
166
- const animateOffset = properties === null || properties === void 0 ? void 0 : properties.animateOffset;
167
- const offsetSpeed = properties === null || properties === void 0 ? void 0 : properties.offsetSpeed;
168
- const generateMipmaps = (properties === null || properties === void 0 ? void 0 : properties.generateMipmaps) !== false;
169
- const minFilter = (properties === null || properties === void 0 ? void 0 : properties.minFilter) || 'LinearMipmapLinearFilter';
170
- const magFilter = (properties === null || properties === void 0 ? void 0 : properties.magFilter) || 'LinearFilter';
171
- const texture = textureName ? (_b = getTexture(textureName)) !== null && _b !== void 0 ? _b : undefined : undefined;
172
- const normalMapTextureName = properties === null || properties === void 0 ? void 0 : properties.normalMapTexture;
173
- const normalScaleProp = properties === null || properties === void 0 ? void 0 : properties.normalScale;
174
- const normalMapTexture = normalMapTextureName ? (_c = getTexture(normalMapTextureName)) !== null && _c !== void 0 ? _c : undefined : undefined;
175
194
  const materialSource = properties !== null && properties !== void 0 ? properties : {};
195
+ const materialType = (_a = materialSource.materialType) !== null && _a !== void 0 ? _a : 'standard';
196
+ const textureName = materialSource.texture;
197
+ const normalMapTextureName = materialSource.normalMapTexture;
198
+ const offset = materialSource.offset;
199
+ const repeat = materialSource.repeat;
200
+ const repeatCount = materialSource.repeatCount;
201
+ const animateOffset = materialSource.animateOffset;
202
+ const offsetSpeed = materialSource.offsetSpeed;
203
+ const generateMipmaps = materialSource.generateMipmaps !== false;
204
+ const minFilter = (_b = materialSource.minFilter) !== null && _b !== void 0 ? _b : 'LinearMipmapLinearFilter';
205
+ const magFilter = (_c = materialSource.magFilter) !== null && _c !== void 0 ? _c : 'LinearFilter';
206
+ const texture = textureName ? getTexture(textureName) : undefined;
207
+ const normalScaleProp = materialSource.normalScale;
208
+ const normalMapTexture = normalMapTextureName ? getTexture(normalMapTextureName) : undefined;
176
209
  // Destructure all material props and separate custom texture handling props
177
210
  const { texture: _texture, offset: _offset, repeat: _repeat, repeatCount: _repeatCount, animateOffset: _animateOffset, offsetSpeed: _offsetSpeed, generateMipmaps: _generateMipmaps, minFilter: _minFilter, magFilter: _magFilter, map: _map, materialType: _materialType, normalMapTexture: _normalMapTexture, normalScale: _normalScale, normalMap: _normalMap, rotation, sizeAttenuation, side: sideProp } = materialSource, materialProps = __rest(materialSource, ["texture", "offset", "repeat", "repeatCount", "animateOffset", "offsetSpeed", "generateMipmaps", "minFilter", "magFilter", "map", "materialType", "normalMapTexture", "normalScale", "normalMap", "rotation", "sizeAttenuation", "side"]);
178
- const sideMap = { FrontSide, BackSide, DoubleSide };
179
- const resolvedSide = sideProp ? ((_d = sideMap[sideProp]) !== null && _d !== void 0 ? _d : FrontSide) : FrontSide;
180
- const minFilterMap = {
181
- NearestFilter,
182
- LinearFilter,
183
- NearestMipmapNearestFilter,
184
- NearestMipmapLinearFilter,
185
- LinearMipmapNearestFilter,
186
- LinearMipmapLinearFilter
187
- };
188
- const magFilterMap = {
189
- NearestFilter,
190
- LinearFilter
191
- };
192
- const animatedOffsetRef = useRef([(_e = offset === null || offset === void 0 ? void 0 : offset[0]) !== null && _e !== void 0 ? _e : 0, (_f = offset === null || offset === void 0 ? void 0 : offset[1]) !== null && _f !== void 0 ? _f : 0]);
211
+ const resolvedSide = sideProp ? (_d = SIDE_MAP[sideProp]) !== null && _d !== void 0 ? _d : FrontSide : FrontSide;
212
+ const resolvedMinFilter = (_e = MIN_FILTER_MAP[minFilter]) !== null && _e !== void 0 ? _e : LinearMipmapLinearFilter;
213
+ const resolvedMagFilter = (_f = MAG_FILTER_MAP[magFilter]) !== null && _f !== void 0 ? _f : LinearFilter;
214
+ const animatedOffsetRef = useRef([(_g = offset === null || offset === void 0 ? void 0 : offset[0]) !== null && _g !== void 0 ? _g : 0, (_h = offset === null || offset === void 0 ? void 0 : offset[1]) !== null && _h !== void 0 ? _h : 0]);
193
215
  const finalTexture = useMemo(() => {
194
- var _a, _b, _c, _d;
195
216
  if (!texture)
196
217
  return undefined;
197
- const t = texture.clone();
198
- if (repeat) {
199
- t.wrapS = t.wrapT = RepeatWrapping;
200
- if (repeatCount)
201
- t.repeat.set(repeatCount[0], repeatCount[1]);
202
- }
203
- else {
204
- t.wrapS = t.wrapT = ClampToEdgeWrapping;
205
- t.repeat.set(1, 1);
206
- }
207
- t.offset.set((_a = offset === null || offset === void 0 ? void 0 : offset[0]) !== null && _a !== void 0 ? _a : 0, (_b = offset === null || offset === void 0 ? void 0 : offset[1]) !== null && _b !== void 0 ? _b : 0);
208
- t.colorSpace = SRGBColorSpace;
209
- t.generateMipmaps = generateMipmaps;
210
- t.minFilter = (_c = minFilterMap[minFilter]) !== null && _c !== void 0 ? _c : LinearMipmapLinearFilter;
211
- t.magFilter = (_d = magFilterMap[magFilter]) !== null && _d !== void 0 ? _d : LinearFilter;
212
- t.needsUpdate = true;
213
- return t;
214
- }, [texture, repeat, repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[0], repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[1], offset === null || offset === void 0 ? void 0 : offset[0], offset === null || offset === void 0 ? void 0 : offset[1], generateMipmaps, minFilter, magFilter]);
215
- animatedOffsetRef.current = [(_g = offset === null || offset === void 0 ? void 0 : offset[0]) !== null && _g !== void 0 ? _g : 0, (_h = offset === null || offset === void 0 ? void 0 : offset[1]) !== null && _h !== void 0 ? _h : 0];
218
+ return cloneConfiguredTexture({
219
+ texture,
220
+ repeat,
221
+ repeatCount,
222
+ offset,
223
+ colorSpace: SRGBColorSpace,
224
+ generateMipmaps,
225
+ minFilter: resolvedMinFilter,
226
+ magFilter: resolvedMagFilter,
227
+ });
228
+ }, [texture, repeat, repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[0], repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[1], offset === null || offset === void 0 ? void 0 : offset[0], offset === null || offset === void 0 ? void 0 : offset[1], generateMipmaps, resolvedMinFilter, resolvedMagFilter]);
229
+ const finalNormalMap = useMemo(() => {
230
+ if (!normalMapTexture)
231
+ return undefined;
232
+ return cloneConfiguredTexture({
233
+ texture: normalMapTexture,
234
+ repeat,
235
+ repeatCount,
236
+ offset,
237
+ colorSpace: LinearSRGBColorSpace,
238
+ generateMipmaps,
239
+ minFilter: resolvedMinFilter,
240
+ magFilter: resolvedMagFilter,
241
+ });
242
+ }, [normalMapTexture, repeat, repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[0], repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[1], offset === null || offset === void 0 ? void 0 : offset[0], offset === null || offset === void 0 ? void 0 : offset[1], generateMipmaps, resolvedMinFilter, resolvedMagFilter]);
243
+ animatedOffsetRef.current = [(_j = offset === null || offset === void 0 ? void 0 : offset[0]) !== null && _j !== void 0 ? _j : 0, (_k = offset === null || offset === void 0 ? void 0 : offset[1]) !== null && _k !== void 0 ? _k : 0];
216
244
  useFrame((_, delta) => {
217
245
  var _a, _b;
218
- if (!finalTexture || !animateOffset)
246
+ if ((!finalTexture && !finalNormalMap) || !animateOffset)
219
247
  return;
220
248
  const nextX = animatedOffsetRef.current[0] + ((_a = offsetSpeed === null || offsetSpeed === void 0 ? void 0 : offsetSpeed[0]) !== null && _a !== void 0 ? _a : 0) * delta;
221
249
  const nextY = animatedOffsetRef.current[1] + ((_b = offsetSpeed === null || offsetSpeed === void 0 ? void 0 : offsetSpeed[1]) !== null && _b !== void 0 ? _b : 0) * delta;
222
250
  animatedOffsetRef.current = [nextX, nextY];
223
- finalTexture.offset.set(nextX, nextY);
251
+ finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.offset.set(nextX, nextY);
252
+ finalNormalMap === null || finalNormalMap === void 0 ? void 0 : finalNormalMap.offset.set(nextX, nextY);
224
253
  });
225
- const finalNormalMap = useMemo(() => {
226
- if (!normalMapTexture)
227
- return undefined;
228
- const t = normalMapTexture.clone();
229
- t.colorSpace = LinearSRGBColorSpace;
230
- t.needsUpdate = true;
231
- return t;
232
- }, [normalMapTexture]);
254
+ const overrides = useMaterialOverrides();
233
255
  if (!properties) {
234
256
  return _jsx("meshStandardNodeMaterial", { attach: "material", color: "red", wireframe: true });
235
257
  }
236
- const overrides = useMaterialOverrides();
258
+ const materialKey = `${materialType}:${textureName !== null && textureName !== void 0 ? textureName : 'none'}:${normalMapTextureName !== null && normalMapTextureName !== void 0 ? normalMapTextureName : 'none'}`;
237
259
  const sharedProps = Object.assign(Object.assign({ map: finalTexture, side: resolvedSide }, materialProps), overrides);
238
260
  if (materialType === 'basic') {
239
- return _jsx("meshBasicNodeMaterial", Object.assign({ attach: "material" }, sharedProps));
261
+ return _jsx("meshBasicNodeMaterial", Object.assign({ attach: "material" }, sharedProps), materialKey);
240
262
  }
241
263
  if (materialType === 'sprite') {
242
- const spriteTransparent = properties.transparent !== false;
243
- return (_jsx("spriteNodeMaterial", Object.assign({ attach: "material", map: finalTexture, color: (_j = properties.color) !== null && _j !== void 0 ? _j : '#ffffff', opacity: (_k = properties.opacity) !== null && _k !== void 0 ? _k : 1, transparent: spriteTransparent, alphaTest: (_l = properties.alphaTest) !== null && _l !== void 0 ? _l : 0, depthTest: (_m = properties.depthTest) !== null && _m !== void 0 ? _m : false, depthWrite: (_o = properties.depthWrite) !== null && _o !== void 0 ? _o : false, toneMapped: (_p = properties.toneMapped) !== null && _p !== void 0 ? _p : true }, overrides, { rotation: rotation !== null && rotation !== void 0 ? rotation : 0, sizeAttenuation: sizeAttenuation !== null && sizeAttenuation !== void 0 ? sizeAttenuation : true })));
264
+ const spriteTransparent = materialSource.transparent !== false;
265
+ return (_jsx("spriteNodeMaterial", Object.assign({ attach: "material", map: finalTexture, color: (_l = materialSource.color) !== null && _l !== void 0 ? _l : '#ffffff', opacity: (_m = materialSource.opacity) !== null && _m !== void 0 ? _m : 1, transparent: spriteTransparent, alphaTest: (_o = materialSource.alphaTest) !== null && _o !== void 0 ? _o : 0, depthTest: (_p = materialSource.depthTest) !== null && _p !== void 0 ? _p : false, depthWrite: (_q = materialSource.depthWrite) !== null && _q !== void 0 ? _q : false, toneMapped: (_r = materialSource.toneMapped) !== null && _r !== void 0 ? _r : true }, overrides, { rotation: rotation !== null && rotation !== void 0 ? rotation : 0, sizeAttenuation: sizeAttenuation !== null && sizeAttenuation !== void 0 ? sizeAttenuation : true }), materialKey));
244
266
  }
245
- return (_jsx("meshStandardNodeMaterial", Object.assign({ attach: "material" }, sharedProps, { normalMap: finalNormalMap, normalScale: finalNormalMap ? [(_q = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[0]) !== null && _q !== void 0 ? _q : 1, (_r = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[1]) !== null && _r !== void 0 ? _r : 1] : undefined })));
267
+ return (_jsx("meshStandardNodeMaterial", Object.assign({ attach: "material" }, sharedProps, { normalMap: finalNormalMap, normalScale: finalNormalMap ? [(_s = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[0]) !== null && _s !== void 0 ? _s : 1, (_t = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[1]) !== null && _t !== void 0 ? _t : 1] : undefined }), materialKey));
246
268
  }
247
269
  const MaterialComponent = {
248
270
  name: 'Material',
@@ -1,3 +1,3 @@
1
- import { Component } from './ComponentRegistry';
1
+ import type { Component } from './ComponentRegistry';
2
2
  declare const ModelComponent: Component;
3
3
  export default ModelComponent;
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ModelPicker } from '../../assetviewer/page';
3
3
  import { useMemo } from 'react';
4
+ import { Mesh } from 'three';
4
5
  import { BooleanField, FieldGroup, Label, ListEditor, NumberInput, SelectInput, StringField } from './Input';
5
6
  import { useAssetRuntime } from '../assetRuntime';
6
7
  import { useEditorContext } from '../PrefabEditor';
@@ -46,23 +47,20 @@ function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
46
47
  // View for Model component
47
48
  function ModelComponentView({ properties, children }) {
48
49
  const { getModel } = useAssetRuntime();
49
- // Instanced models are handled elsewhere (GameInstance), so only render non-instanced here
50
- if (!properties.filename || properties.instanced)
51
- return _jsx(_Fragment, { children: children });
52
- const sourceModel = getModel(properties.filename);
50
+ const sourceModel = properties.filename ? getModel(properties.filename) : null;
53
51
  // Clone model once and set up shadows - memoized to avoid cloning on every render
54
52
  const clonedModel = useMemo(() => {
55
- if (!sourceModel)
53
+ if (!sourceModel || !properties.filename || properties.instanced)
56
54
  return null;
57
55
  const clone = sourceModel.clone();
58
56
  clone.traverse((obj) => {
59
- if (obj.isMesh) {
57
+ if (obj instanceof Mesh) {
60
58
  obj.castShadow = true;
61
59
  obj.receiveShadow = true;
62
60
  }
63
61
  });
64
62
  return clone;
65
- }, [sourceModel]);
63
+ }, [properties.filename, properties.instanced, sourceModel]);
66
64
  if (!clonedModel)
67
65
  return _jsx(_Fragment, { children: children });
68
66
  return _jsx("primitive", { object: clonedModel, children: children });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.95",
3
+ "version": "0.0.96",
4
4
  "description": "high performance 3D game engine built in React",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",