react-three-game 0.0.95 → 0.0.97

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,
@@ -31,32 +33,11 @@ export function normalizeRepeatAxes(value) {
31
33
  }, []);
32
34
  return normalized.length > 0 ? normalized : DEFAULT_REPEAT_AXES;
33
35
  }
34
- function toVector3Tuple(value, fallback) {
35
- if (!Array.isArray(value) || value.length !== 3)
36
- return fallback;
37
- return value.map((entry, index) => {
38
- const next = typeof entry === 'number' ? entry : Number(entry);
39
- return Number.isFinite(next) ? next : fallback[index];
40
- });
41
- }
42
36
  export function getRepeatAxesFromModelProperties(properties) {
43
- var _a;
44
37
  if (Array.isArray(properties.repeatAxes)) {
45
38
  return normalizeRepeatAxes(properties.repeatAxes);
46
39
  }
47
- const repeatCount = toVector3Tuple(properties.repeatCount, [1, 1, 1]).map(value => Math.max(1, Math.floor(value)));
48
- const repeatOffset = toVector3Tuple(properties.repeatOffset, [1, 1, 1]);
49
- const legacyAxes = [];
50
- if ((_a = properties.repeatX) !== null && _a !== void 0 ? _a : true) {
51
- legacyAxes.push({ axis: 'x', count: repeatCount[0], offset: repeatOffset[0] });
52
- }
53
- if (properties.repeatY) {
54
- legacyAxes.push({ axis: 'y', count: repeatCount[1], offset: repeatOffset[1] });
55
- }
56
- if (properties.repeatZ) {
57
- legacyAxes.push({ axis: 'z', count: repeatCount[2], offset: repeatOffset[2] });
58
- }
59
- return legacyAxes.length > 0 ? legacyAxes : DEFAULT_REPEAT_AXES;
40
+ return DEFAULT_REPEAT_AXES;
60
41
  }
61
42
  // Helper functions for comparison
62
43
  function arrayEquals(a, b) {
@@ -141,7 +122,7 @@ export function GameInstanceProvider({ children, models, onSelect, onClick, regi
141
122
  const rootInverse = new Matrix4().copy(model.matrixWorld).invert();
142
123
  let partIndex = 0;
143
124
  model.traverse((obj) => {
144
- if (obj.isMesh) {
125
+ if (obj instanceof Mesh) {
145
126
  // Clone geometry and bake relative transform
146
127
  const geom = obj.geometry.clone();
147
128
  geom.applyMatrix4(obj.matrixWorld.clone().premultiply(rootInverse));
@@ -157,7 +138,9 @@ export function GameInstanceProvider({ children, models, onSelect, onClick, regi
157
138
  // Cleanup geometries when models change
158
139
  useEffect(() => {
159
140
  return () => {
160
- Object.values(flatMeshes).forEach(mesh => mesh.geometry.dispose());
141
+ Object.values(flatMeshes).forEach(mesh => {
142
+ mesh.geometry.dispose();
143
+ });
161
144
  };
162
145
  }, [flatMeshes]);
163
146
  const instances = useMemo(() => Object.values(instancesById), [instancesById]);
@@ -194,12 +177,16 @@ export function GameInstanceProvider({ children, models, onSelect, onClick, regi
194
177
  })] }));
195
178
  }
196
179
  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]);
180
+ const instanceEntries = useMemo(() => Array.from({ length: partCount }, (_, i) => {
181
+ const partKey = `${modelKey}__${i}`;
182
+ const Component = instancesMap[partKey];
183
+ return Component ? { partKey, Component } : null;
184
+ }).filter((entry) => Boolean(entry)), [instancesMap, modelKey, partCount]);
198
185
  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))) }));
186
+ 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
187
  }
201
188
  // Individual instance item with its own click state
202
- function InstanceGroupItem({ instance, InstanceComponents, onSelect, onClick, registerRef, selectedId, editMode }) {
189
+ function InstanceGroupItem({ instance, instanceEntries, onSelect, onClick, registerRef, selectedId, editMode }) {
203
190
  const groupRef = useRef(null);
204
191
  const isLocked = Boolean(instance.locked);
205
192
  const isSelected = selectedId === instance.id || selectedId === instance.sourceId;
@@ -217,24 +204,26 @@ function InstanceGroupItem({ instance, InstanceComponents, onSelect, onClick, re
217
204
  },
218
205
  });
219
206
  // Use BoxHelper when object is selected in edit mode
220
- useHelper(editMode && isSelected ? groupRef : null, BoxHelper, 'cyan');
207
+ const helperTarget = editMode && isSelected && groupRef.current
208
+ ? { current: groupRef.current }
209
+ : null;
210
+ useHelper(helperTarget, BoxHelper, 'cyan');
221
211
  useEffect(() => {
222
212
  if (editMode)
223
213
  return;
224
214
  registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, groupRef.current);
225
215
  return () => registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, null);
226
216
  }, [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)) })));
217
+ 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
218
  }
229
219
  export function useInstanceCheck(id) {
220
+ var _a;
230
221
  const ctx = useContext(GameInstanceContext);
231
- return ctx ? useStore(ctx.store, state => Boolean(state.instancesById[id] || state.sourceInstanceIdsById[id])) : false;
222
+ const store = (_a = ctx === null || ctx === void 0 ? void 0 : ctx.store) !== null && _a !== void 0 ? _a : EMPTY_INSTANCE_STORE;
223
+ return useStore(store, state => Boolean(state.instancesById[id] || state.sourceInstanceIdsById[id]));
232
224
  }
233
- export const GameInstance = React.forwardRef(({ id, sourceId, modelUrl, locked = false, position, rotation, scale, visible = true, onClick: _onClick, }, ref) => {
225
+ export function GameInstance({ id, sourceId, modelUrl, locked = false, position, rotation, scale, visible = true, onClick: _onClick, }) {
234
226
  const ctx = useContext(GameInstanceContext);
235
- const [positionX, positionY, positionZ] = position;
236
- const [rotationX, rotationY, rotationZ] = rotation;
237
- const [scaleX, scaleY, scaleZ] = scale;
238
227
  const instance = useMemo(() => ({
239
228
  id,
240
229
  sourceId: sourceId !== null && sourceId !== void 0 ? sourceId : id,
@@ -244,22 +233,7 @@ export const GameInstance = React.forwardRef(({ id, sourceId, modelUrl, locked =
244
233
  position,
245
234
  rotation,
246
235
  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
- ]);
236
+ }), [id, sourceId, locked, visible, modelUrl, position, rotation, scale]);
263
237
  useEffect(() => {
264
238
  if (!ctx)
265
239
  return;
@@ -269,6 +243,6 @@ export const GameInstance = React.forwardRef(({ id, sourceId, modelUrl, locked =
269
243
  return () => {
270
244
  removeInstance(instance.id);
271
245
  };
272
- }, [ctx === null || ctx === void 0 ? void 0 : ctx.store, instance]);
246
+ }, [ctx, instance]);
273
247
  return null;
274
- });
248
+ }
@@ -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 {
@@ -34,7 +34,10 @@ function isObjectAttachedToRoot(root, object) {
34
34
  function SelectionHelper({ object }) {
35
35
  const objectRef = useRef(null);
36
36
  objectRef.current = object;
37
- useHelper(object ? objectRef : null, BoxHelper, "cyan");
37
+ const helperTarget = objectRef.current
38
+ ? { current: objectRef.current }
39
+ : null;
40
+ useHelper(helperTarget, BoxHelper, "cyan");
38
41
  return null;
39
42
  }
40
43
  export const EditorContext = createContext(null);
@@ -79,6 +82,10 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
79
82
  const transformControlsRef = useRef(null);
80
83
  const onChangeRef = useRef(onChange);
81
84
  const isEditMode = mode === PrefabEditorMode.Edit;
85
+ const detachTransformControls = useCallback(() => {
86
+ var _a;
87
+ (_a = transformControlsRef.current) === null || _a === void 0 ? void 0 : _a.detach();
88
+ }, []);
82
89
  const getPrefab = useCallback(() => denormalizePrefab(prefabStore.getState()), [prefabStore]);
83
90
  const getNode = useCallback((nodeId) => { var _a; return (_a = prefabStore.getState().nodesById[nodeId]) !== null && _a !== void 0 ? _a : null; }, [prefabStore]);
84
91
  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 +165,13 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
158
165
  updateMode(initialMode);
159
166
  }, [initialMode, updateMode]);
160
167
  const loadPrefab = useCallback((prefab, options) => {
161
- var _a, _b;
162
- (_a = transformControlsRef.current) === null || _a === void 0 ? void 0 : _a.detach();
168
+ var _a;
169
+ detachTransformControls();
163
170
  const before = prefabStore.getState();
164
171
  prefabStore.getState().replacePrefab(prefab);
165
172
  const after = prefabStore.getState();
166
173
  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);
174
+ (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, prefab);
168
175
  }
169
176
  if (options === null || options === void 0 ? void 0 : options.resetHistory) {
170
177
  setSelectedId(null);
@@ -175,7 +182,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
175
182
  else {
176
183
  setSelectedId(prev => prev && prefabStore.getState().nodesById[prev] ? prev : null);
177
184
  }
178
- }, [prefabStore]);
185
+ }, [detachTransformControls, prefabStore]);
179
186
  useEffect(() => {
180
187
  if (initialPrefab)
181
188
  loadPrefab(initialPrefab, { resetHistory: true, notifyChange: false });
@@ -205,17 +212,25 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
205
212
  const importPrefab = useCallback((prefab) => {
206
213
  add(regenerateIds(prefab.root));
207
214
  }, [add]);
208
- const applyHistory = (index) => {
209
- var _a, _b;
210
- (_a = transformControlsRef.current) === null || _a === void 0 ? void 0 : _a.detach();
215
+ const applyHistory = useCallback((index) => {
216
+ var _a;
217
+ detachTransformControls();
211
218
  prefabStore.getState().replacePrefab(history[index]);
212
- (_b = onChangeRef.current) === null || _b === void 0 ? void 0 : _b.call(onChangeRef, history[index]);
219
+ (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, history[index]);
213
220
  historyIndexRef.current = index;
214
221
  setHistoryIndex(index);
215
222
  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);
223
+ }, [detachTransformControls, history, prefabStore]);
224
+ const undo = useCallback(() => {
225
+ if (historyIndex > 0) {
226
+ applyHistory(historyIndex - 1);
227
+ }
228
+ }, [applyHistory, historyIndex]);
229
+ const redo = useCallback(() => {
230
+ if (historyIndex < history.length - 1) {
231
+ applyHistory(historyIndex + 1);
232
+ }
233
+ }, [applyHistory, history.length, historyIndex]);
219
234
  useEffect(() => {
220
235
  if (!isEditMode)
221
236
  return;
@@ -233,7 +248,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
233
248
  };
234
249
  window.addEventListener('keydown', handleKeyDown);
235
250
  return () => window.removeEventListener('keydown', handleKeyDown);
236
- }, [isEditMode, historyIndex, history]);
251
+ }, [isEditMode, redo, undo]);
237
252
  const handleScreenshot = useCallback(() => {
238
253
  const canvas = canvasRef.current;
239
254
  if (!canvas)
@@ -383,7 +398,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
383
398
  }
384
399
  (_d = canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed) === null || _d === void 0 ? void 0 : _d.call(canvasProps, event);
385
400
  }
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 }))] }))] }) }) });
401
+ : 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
402
  });
388
403
  PrefabEditor.displayName = "PrefabEditor";
389
404
  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,14 +358,9 @@ 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);
345
- const groupProps = Object.assign(Object.assign({}, metadataProps), { visible: nodeVisible, position: localTransform.position, rotation: localTransform.rotation, scale: localTransform.scale });
363
+ const isLocked = Boolean(gameObject === null || gameObject === void 0 ? void 0 : gameObject.locked);
346
364
  const modelUrl = (_b = (_a = analyzedComponents.model) === null || _a === void 0 ? void 0 : _a.properties) === null || _b === void 0 ? void 0 : _b.filename;
347
365
  const instances = useMemo(() => buildRepeatedInstances(gameObject, parentMatrix, modelUrl), [gameObject, modelUrl, parentMatrix]);
348
366
  const groupRef = useRef(null);
@@ -353,9 +371,15 @@ function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef,
353
371
  }
354
372
  }, [editMode, nodeId, registerRef]);
355
373
  const editClickHandlers = useClickValid(!!editMode && !isLocked, (event) => {
374
+ if (!gameObject)
375
+ return;
356
376
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(nodeId);
357
377
  onEditNodeClick === null || onEditNodeClick === void 0 ? void 0 : onEditNodeClick(event, gameObject);
358
378
  });
379
+ if (!gameObject)
380
+ return null;
381
+ const nodeVisible = isVisible && !gameObject.hidden;
382
+ const groupProps = Object.assign(Object.assign({}, getNodeMetadataProps(gameObject)), { visible: nodeVisible, position: localTransform.position, rotation: localTransform.rotation, scale: localTransform.scale });
359
383
  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
384
  if (editMode) {
361
385
  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 +389,17 @@ function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef,
365
389
  function StandardNode({ nodeId, selectedId, onSelect, onClick, onEditNodeClick, registerRef, loadedModels, editMode, parentMatrix = IDENTITY, isVisible = true, }) {
366
390
  const gameObject = usePrefabNode(nodeId);
367
391
  const childIds = usePrefabChildIds(nodeId);
368
- if (!gameObject)
369
- return null;
370
- const analyzedComponents = useMemo(() => analyzeNodeComponents(gameObject), [gameObject]);
392
+ const analyzedComponents = useMemo(() => gameObject ? analyzeNodeComponents(gameObject) : EMPTY_NODE_COMPONENTS, [gameObject]);
371
393
  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);
394
+ const isLocked = Boolean(gameObject === null || gameObject === void 0 ? void 0 : gameObject.locked);
376
395
  const groupRef = useRef(null);
377
396
  const handleGroupRef = useCallback((object) => {
378
397
  groupRef.current = object;
379
398
  registerRef(nodeId, object);
380
399
  }, [nodeId, registerRef]);
381
400
  const editClickHandlers = useClickValid(!!editMode && !isLocked, (event) => {
401
+ if (!gameObject)
402
+ return;
382
403
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(nodeId);
383
404
  onEditNodeClick === null || onEditNodeClick === void 0 ? void 0 : onEditNodeClick(event, gameObject);
384
405
  });
@@ -391,7 +412,10 @@ function StandardNode({ nodeId, selectedId, onSelect, onClick, onEditNodeClick,
391
412
  }
392
413
  : undefined;
393
414
  const world = parentMatrix.clone().multiply(compose(gameObject));
394
- const ready = isNodeReady(analyzedComponents.model, loadedModels);
415
+ if (!gameObject)
416
+ return null;
417
+ const nodeVisible = isVisible && !gameObject.hidden;
418
+ const metadataProps = getNodeMetadataProps(gameObject);
395
419
  const transform = getNodeTransformProps(gameObject);
396
420
  const transformProps = {
397
421
  position: transform.position,
@@ -422,7 +446,7 @@ function getModelRepeatSettings(node) {
422
446
  };
423
447
  }
424
448
  function buildRepeatedInstances(gameObject, parentMatrix, modelUrl) {
425
- if (!modelUrl)
449
+ if (!gameObject || !modelUrl)
426
450
  return [];
427
451
  const transform = getNodeTransformProps(gameObject);
428
452
  const repeat = getModelRepeatSettings(gameObject);
@@ -1,3 +1,3 @@
1
- import { Component } from './ComponentRegistry';
1
+ import type { Component } from './ComponentRegistry';
2
2
  declare const CameraComponent: Component;
3
3
  export default CameraComponent;
@@ -39,10 +39,13 @@ function CameraComponentView({ properties, children }) {
39
39
  const halfWidth = halfHeight * aspect;
40
40
  const perspectiveCameraRef = useRef(null);
41
41
  const orthographicCameraRef = useRef(null);
42
- const activeCameraRef = projection === 'orthographic'
43
- ? orthographicCameraRef
44
- : perspectiveCameraRef;
45
- useHelper(editMode && isSelected ? activeCameraRef : null, CameraHelper);
42
+ const activeCamera = projection === 'orthographic'
43
+ ? orthographicCameraRef.current
44
+ : perspectiveCameraRef.current;
45
+ const helperTarget = editMode && isSelected && activeCamera
46
+ ? { current: activeCamera }
47
+ : null;
48
+ useHelper(helperTarget, CameraHelper);
46
49
  useFrame(() => {
47
50
  if (!editMode || !isSelected)
48
51
  return;
@@ -1,11 +1,13 @@
1
- import { FC } from "react";
2
- import { ComponentData, GameObject } from "../types";
1
+ import type { FC } from "react";
2
+ import type { ComponentData, GameObject } from "../types";
3
3
  export type AssetRef = {
4
4
  type: "model" | "texture" | "sound";
5
5
  path: string;
6
6
  };
7
+ export declare function assetRef(type: AssetRef["type"], path: unknown): AssetRef | null;
8
+ export declare function assetRefs(...refs: Array<AssetRef | null | undefined>): AssetRef[];
7
9
  /** Props every component View receives from the renderer. */
8
- export interface ComponentViewProps<P = Record<string, any>> {
10
+ export interface ComponentViewProps<P = Record<string, unknown>> {
9
11
  /** This component's own data from the prefab JSON. */
10
12
  properties: P;
11
13
  /** Children to render for components that wrap the current subtree. */
@@ -22,15 +24,15 @@ export interface Component {
22
24
  Editor: FC<{
23
25
  node?: GameObject;
24
26
  component: ComponentData;
25
- onUpdate: (newComp: any) => void;
27
+ onUpdate: (newComp: Record<string, unknown>) => void;
26
28
  basePath?: string;
27
29
  }>;
28
- defaultProperties: any;
30
+ defaultProperties: Record<string, unknown>;
29
31
  View?: FC<ComponentViewProps>;
30
32
  /** Declare which asset paths this component references (for asset loading). */
31
- getAssetRefs?: (properties: Record<string, any>) => AssetRef[];
33
+ getAssetRefs?: (properties: Record<string, unknown>) => AssetRef[];
32
34
  }
33
35
  export declare function registerComponent(component: Component): void;
34
36
  export declare function getComponentDef(name: string): Component | undefined;
35
37
  export declare function getAllComponentDefs(): Record<string, Component>;
36
- export declare function getComponentAssetRefs(componentType: string, properties: Record<string, any>): AssetRef[];
38
+ export declare function getComponentAssetRefs(componentType: string, properties: Record<string, unknown>): AssetRef[];
@@ -1,3 +1,9 @@
1
+ export function assetRef(type, path) {
2
+ return typeof path === "string" ? { type, path } : null;
3
+ }
4
+ export function assetRefs(...refs) {
5
+ return refs.filter((ref) => ref != null);
6
+ }
1
7
  const REGISTRY = {};
2
8
  export function registerComponent(component) {
3
9
  REGISTRY[component.name] = component;
@@ -1,3 +1,3 @@
1
- import { Component } from "./ComponentRegistry";
1
+ import type { Component } from "./ComponentRegistry";
2
2
  declare const DirectionalLightComponent: Component;
3
3
  export default DirectionalLightComponent;
@@ -122,7 +122,10 @@ function DirectionalLightView({ properties, children }) {
122
122
  const targetRef = useRef(null);
123
123
  const shadowCameraRef = useRef(null);
124
124
  const [shadowCamera, setShadowCamera] = useState(null);
125
- useHelper(editMode && isSelected && castShadow ? shadowCameraRef : null, CameraHelper);
125
+ const helperTarget = editMode && isSelected && castShadow && shadowCameraRef.current
126
+ ? { current: shadowCameraRef.current }
127
+ : null;
128
+ useHelper(helperTarget, CameraHelper);
126
129
  // Use a local target object so node transforms rotate the light direction naturally.
127
130
  useEffect(() => {
128
131
  if (directionalLightRef.current && targetRef.current) {
@@ -131,7 +134,7 @@ function DirectionalLightView({ properties, children }) {
131
134
  shadowCameraRef.current = nextShadowCamera;
132
135
  setShadowCamera(castShadow ? nextShadowCamera : null);
133
136
  }
134
- }, [castShadow]);
137
+ });
135
138
  useEffect(() => {
136
139
  var _a;
137
140
  const shadow = (_a = directionalLightRef.current) === null || _a === void 0 ? void 0 : _a.shadow;
@@ -139,19 +142,7 @@ function DirectionalLightView({ properties, children }) {
139
142
  return;
140
143
  shadow.needsUpdate = true;
141
144
  shadow.camera.updateProjectionMatrix();
142
- }, [
143
- castShadow,
144
- shadowMapSize,
145
- shadowBias,
146
- shadowNormalBias,
147
- shadowAutoUpdate,
148
- shadowCameraNear,
149
- shadowCameraFar,
150
- shadowCameraTop,
151
- shadowCameraBottom,
152
- shadowCameraLeft,
153
- shadowCameraRight,
154
- ]);
145
+ });
155
146
  useFrame(() => {
156
147
  if (!directionalLightRef.current || !targetRef.current)
157
148
  return;
@@ -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;
@@ -13,6 +13,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
13
13
  import { createContext, useContext, useMemo, useRef } from 'react';
14
14
  import { extend } from '@react-three/fiber';
15
15
  import { useFrame } from '@react-three/fiber';
16
+ import { assetRef, assetRefs } from './ComponentRegistry';
16
17
  import { FieldRenderer, Label, NumberInput } from './Input';
17
18
  import { useAssetRuntime } from '../assetRuntime';
18
19
  import { MeshBasicNodeMaterial, MeshStandardNodeMaterial, SpriteNodeMaterial } from 'three/webgpu';
@@ -24,6 +25,39 @@ function Vector2Editor({ label, value, onChange, min, max, step, }) {
24
25
  }
25
26
  const EMPTY_MATERIAL_OVERRIDES = Object.freeze({});
26
27
  const MaterialOverridesContext = createContext(EMPTY_MATERIAL_OVERRIDES);
28
+ const SIDE_MAP = { FrontSide, BackSide, DoubleSide };
29
+ const MIN_FILTER_MAP = {
30
+ NearestFilter,
31
+ LinearFilter,
32
+ NearestMipmapNearestFilter,
33
+ NearestMipmapLinearFilter,
34
+ LinearMipmapNearestFilter,
35
+ LinearMipmapLinearFilter,
36
+ };
37
+ const MAG_FILTER_MAP = {
38
+ NearestFilter,
39
+ LinearFilter,
40
+ };
41
+ function cloneConfiguredTexture({ texture, repeat, repeatCount, offset, colorSpace, generateMipmaps, minFilter, magFilter, }) {
42
+ var _a, _b;
43
+ const clonedTexture = texture.clone();
44
+ if (repeat) {
45
+ clonedTexture.wrapS = clonedTexture.wrapT = RepeatWrapping;
46
+ if (repeatCount)
47
+ clonedTexture.repeat.set(repeatCount[0], repeatCount[1]);
48
+ }
49
+ else {
50
+ clonedTexture.wrapS = clonedTexture.wrapT = ClampToEdgeWrapping;
51
+ clonedTexture.repeat.set(1, 1);
52
+ }
53
+ 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);
54
+ clonedTexture.colorSpace = colorSpace;
55
+ clonedTexture.generateMipmaps = generateMipmaps;
56
+ clonedTexture.minFilter = minFilter;
57
+ clonedTexture.magFilter = magFilter;
58
+ clonedTexture.needsUpdate = true;
59
+ return clonedTexture;
60
+ }
27
61
  export function useMaterialOverrides() {
28
62
  return useContext(MaterialOverridesContext);
29
63
  }
@@ -37,7 +71,7 @@ extend({
37
71
  MeshStandardNodeMaterial,
38
72
  SpriteNodeMaterial,
39
73
  });
40
- function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
74
+ function MaterialComponentEditor({ component, onUpdate, basePath = "", }) {
41
75
  var _a;
42
76
  const materialType = (_a = component.properties.materialType) !== null && _a !== void 0 ? _a : 'standard';
43
77
  const hasTexture = !!component.properties.texture;
@@ -155,94 +189,83 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
155
189
  }
156
190
  // View for Material component
157
191
  function MaterialComponentView({ properties: rawProps }) {
158
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
192
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
159
193
  const { getTexture } = useAssetRuntime();
160
194
  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
195
  const materialSource = properties !== null && properties !== void 0 ? properties : {};
196
+ const materialType = (_a = materialSource.materialType) !== null && _a !== void 0 ? _a : 'standard';
197
+ const textureName = materialSource.texture;
198
+ const normalMapTextureName = materialSource.normalMapTexture;
199
+ const offset = materialSource.offset;
200
+ const repeat = materialSource.repeat;
201
+ const repeatCount = materialSource.repeatCount;
202
+ const animateOffset = materialSource.animateOffset;
203
+ const offsetSpeed = materialSource.offsetSpeed;
204
+ const generateMipmaps = materialSource.generateMipmaps !== false;
205
+ const minFilter = (_b = materialSource.minFilter) !== null && _b !== void 0 ? _b : 'LinearMipmapLinearFilter';
206
+ const magFilter = (_c = materialSource.magFilter) !== null && _c !== void 0 ? _c : 'LinearFilter';
207
+ const texture = textureName ? getTexture(textureName) : undefined;
208
+ const normalScaleProp = materialSource.normalScale;
209
+ const normalMapTexture = normalMapTextureName ? getTexture(normalMapTextureName) : undefined;
176
210
  // Destructure all material props and separate custom texture handling props
177
211
  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]);
212
+ const resolvedSide = sideProp ? (_d = SIDE_MAP[sideProp]) !== null && _d !== void 0 ? _d : FrontSide : FrontSide;
213
+ const resolvedMinFilter = (_e = MIN_FILTER_MAP[minFilter]) !== null && _e !== void 0 ? _e : LinearMipmapLinearFilter;
214
+ const resolvedMagFilter = (_f = MAG_FILTER_MAP[magFilter]) !== null && _f !== void 0 ? _f : LinearFilter;
215
+ 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
216
  const finalTexture = useMemo(() => {
194
- var _a, _b, _c, _d;
195
217
  if (!texture)
196
218
  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];
219
+ return cloneConfiguredTexture({
220
+ texture,
221
+ repeat,
222
+ repeatCount,
223
+ offset,
224
+ colorSpace: SRGBColorSpace,
225
+ generateMipmaps,
226
+ minFilter: resolvedMinFilter,
227
+ magFilter: resolvedMagFilter,
228
+ });
229
+ }, [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]);
230
+ const finalNormalMap = useMemo(() => {
231
+ if (!normalMapTexture)
232
+ return undefined;
233
+ return cloneConfiguredTexture({
234
+ texture: normalMapTexture,
235
+ repeat,
236
+ repeatCount,
237
+ offset,
238
+ colorSpace: LinearSRGBColorSpace,
239
+ generateMipmaps,
240
+ minFilter: resolvedMinFilter,
241
+ magFilter: resolvedMagFilter,
242
+ });
243
+ }, [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]);
244
+ 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
245
  useFrame((_, delta) => {
217
246
  var _a, _b;
218
- if (!finalTexture || !animateOffset)
247
+ if ((!finalTexture && !finalNormalMap) || !animateOffset)
219
248
  return;
220
249
  const nextX = animatedOffsetRef.current[0] + ((_a = offsetSpeed === null || offsetSpeed === void 0 ? void 0 : offsetSpeed[0]) !== null && _a !== void 0 ? _a : 0) * delta;
221
250
  const nextY = animatedOffsetRef.current[1] + ((_b = offsetSpeed === null || offsetSpeed === void 0 ? void 0 : offsetSpeed[1]) !== null && _b !== void 0 ? _b : 0) * delta;
222
251
  animatedOffsetRef.current = [nextX, nextY];
223
- finalTexture.offset.set(nextX, nextY);
252
+ finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.offset.set(nextX, nextY);
253
+ finalNormalMap === null || finalNormalMap === void 0 ? void 0 : finalNormalMap.offset.set(nextX, nextY);
224
254
  });
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]);
255
+ const overrides = useMaterialOverrides();
233
256
  if (!properties) {
234
257
  return _jsx("meshStandardNodeMaterial", { attach: "material", color: "red", wireframe: true });
235
258
  }
236
- const overrides = useMaterialOverrides();
259
+ const materialKey = `${materialType}:${textureName !== null && textureName !== void 0 ? textureName : 'none'}:${normalMapTextureName !== null && normalMapTextureName !== void 0 ? normalMapTextureName : 'none'}`;
237
260
  const sharedProps = Object.assign(Object.assign({ map: finalTexture, side: resolvedSide }, materialProps), overrides);
238
261
  if (materialType === 'basic') {
239
- return _jsx("meshBasicNodeMaterial", Object.assign({ attach: "material" }, sharedProps));
262
+ return _jsx("meshBasicNodeMaterial", Object.assign({ attach: "material" }, sharedProps), materialKey);
240
263
  }
241
264
  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 })));
265
+ const spriteTransparent = materialSource.transparent !== false;
266
+ 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
267
  }
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 })));
268
+ 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
269
  }
247
270
  const MaterialComponent = {
248
271
  name: 'Material',
@@ -262,13 +285,6 @@ const MaterialComponent = {
262
285
  metalness: 0,
263
286
  roughness: 1
264
287
  },
265
- getAssetRefs: (properties) => {
266
- const refs = [];
267
- if (properties.texture)
268
- refs.push({ type: 'texture', path: properties.texture });
269
- if (properties.normalMapTexture)
270
- refs.push({ type: 'texture', path: properties.normalMapTexture });
271
- return refs;
272
- },
288
+ getAssetRefs: (properties) => assetRefs(assetRef('texture', properties.texture), assetRef('texture', properties.normalMapTexture)),
273
289
  };
274
290
  export default MaterialComponent;
@@ -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,8 @@
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';
5
+ import { assetRef, assetRefs } from './ComponentRegistry';
4
6
  import { BooleanField, FieldGroup, Label, ListEditor, NumberInput, SelectInput, StringField } from './Input';
5
7
  import { useAssetRuntime } from '../assetRuntime';
6
8
  import { useEditorContext } from '../PrefabEditor';
@@ -46,23 +48,20 @@ function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
46
48
  // View for Model component
47
49
  function ModelComponentView({ properties, children }) {
48
50
  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);
51
+ const sourceModel = properties.filename ? getModel(properties.filename) : null;
53
52
  // Clone model once and set up shadows - memoized to avoid cloning on every render
54
53
  const clonedModel = useMemo(() => {
55
- if (!sourceModel)
54
+ if (!sourceModel || !properties.filename || properties.instanced)
56
55
  return null;
57
56
  const clone = sourceModel.clone();
58
57
  clone.traverse((obj) => {
59
- if (obj.isMesh) {
58
+ if (obj instanceof Mesh) {
60
59
  obj.castShadow = true;
61
60
  obj.receiveShadow = true;
62
61
  }
63
62
  });
64
63
  return clone;
65
- }, [sourceModel]);
64
+ }, [properties.filename, properties.instanced, sourceModel]);
66
65
  if (!clonedModel)
67
66
  return _jsx(_Fragment, { children: children });
68
67
  return _jsx("primitive", { object: clonedModel, children: children });
@@ -72,10 +71,6 @@ const ModelComponent = {
72
71
  Editor: ModelComponentEditor,
73
72
  View: ModelComponentView,
74
73
  defaultProperties: {},
75
- getAssetRefs: (properties) => {
76
- if (properties.filename)
77
- return [{ type: 'model', path: properties.filename }];
78
- return [];
79
- },
74
+ getAssetRefs: (properties) => assetRefs(assetRef('model', properties.filename)),
80
75
  };
81
76
  export default ModelComponent;
@@ -1,3 +1,3 @@
1
- import { Component } from './ComponentRegistry';
1
+ import type { Component } from './ComponentRegistry';
2
2
  declare const PointLightComponent: Component;
3
3
  export default PointLightComponent;
@@ -37,7 +37,10 @@ function PointLightView({ properties, children }) {
37
37
  const shadowCameraNear = merged.shadowCameraNear;
38
38
  const shadowCameraFar = merged.shadowCameraFar;
39
39
  const lightRef = useRef(null);
40
- useHelper(editMode && isSelected ? lightRef : null, PointLightHelper, 0.5, color);
40
+ const helperTarget = editMode && isSelected && lightRef.current
41
+ ? { current: lightRef.current }
42
+ : null;
43
+ useHelper(helperTarget, PointLightHelper, 0.5, color);
41
44
  useEffect(() => {
42
45
  var _a;
43
46
  const shadow = (_a = lightRef.current) === null || _a === void 0 ? void 0 : _a.shadow;
@@ -45,7 +48,7 @@ function PointLightView({ properties, children }) {
45
48
  return;
46
49
  shadow.needsUpdate = true;
47
50
  shadow.camera.updateProjectionMatrix();
48
- }, [castShadow, shadowMapSize, shadowBias, shadowNormalBias, shadowAutoUpdate, shadowCameraNear, shadowCameraFar]);
51
+ });
49
52
  return (_jsxs(_Fragment, { children: [_jsx("pointLight", { ref: lightRef, color: color, intensity: intensity, distance: distance, decay: decay, castShadow: castShadow, "shadow-mapSize-width": shadowMapSize, "shadow-mapSize-height": shadowMapSize, "shadow-bias": shadowBias, "shadow-normalBias": shadowNormalBias, "shadow-autoUpdate": shadowAutoUpdate, "shadow-camera-near": shadowCameraNear, "shadow-camera-far": shadowCameraFar }), editMode && isSelected ? (_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.2, 10, 8] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] })) : null, children] }));
50
53
  }
51
54
  const PointLightComponent = {
@@ -1,3 +1,3 @@
1
- import { Component } from "./ComponentRegistry";
1
+ import type { Component } from "./ComponentRegistry";
2
2
  declare const SpotLightComponent: Component;
3
3
  export default SpotLightComponent;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { assetRef, assetRefs } from "./ComponentRegistry";
2
3
  import { useHelper } from "@react-three/drei";
3
4
  import { useRef, useEffect } from "react";
4
5
  import { BooleanField, ColorField, Label, NumberField, Vector3Input } from "./Input";
@@ -50,12 +51,15 @@ function SpotLightView({ properties, children }) {
50
51
  const textureMap = merged.map ? (_a = getTexture(merged.map)) !== null && _a !== void 0 ? _a : undefined : undefined;
51
52
  const spotLightRef = useRef(null);
52
53
  const targetRef = useRef(null);
53
- useHelper(editMode && isSelected ? spotLightRef : null, SpotLightHelper, color);
54
+ const helperTarget = editMode && isSelected && spotLightRef.current
55
+ ? { current: spotLightRef.current }
56
+ : null;
57
+ useHelper(helperTarget, SpotLightHelper, color);
54
58
  useEffect(() => {
55
59
  if (spotLightRef.current && targetRef.current) {
56
60
  spotLightRef.current.target = targetRef.current;
57
61
  }
58
- }, [castShadow]);
62
+ });
59
63
  useEffect(() => {
60
64
  var _a;
61
65
  const shadow = (_a = spotLightRef.current) === null || _a === void 0 ? void 0 : _a.shadow;
@@ -63,7 +67,7 @@ function SpotLightView({ properties, children }) {
63
67
  return;
64
68
  shadow.needsUpdate = true;
65
69
  shadow.camera.updateProjectionMatrix();
66
- }, [castShadow, shadowMapSize, shadowBias, shadowNormalBias, shadowAutoUpdate, shadowCameraNear, shadowCameraFar]);
70
+ });
67
71
  useFrame(() => {
68
72
  var _a;
69
73
  if ((_a = spotLightRef.current) === null || _a === void 0 ? void 0 : _a.target) {
@@ -77,10 +81,6 @@ const SpotLightComponent = {
77
81
  Editor: SpotLightComponentEditor,
78
82
  View: SpotLightView,
79
83
  defaultProperties: {},
80
- getAssetRefs: (properties) => {
81
- if (properties.map)
82
- return [{ type: 'texture', path: properties.map }];
83
- return [];
84
- },
84
+ getAssetRefs: (properties) => assetRefs(assetRef('texture', properties.map)),
85
85
  };
86
86
  export default SpotLightComponent;
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.97",
4
4
  "description": "high performance 3D game engine built in React",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",