rubiks-cube-3d 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/dist/core-CRe_v4Mo.cjs +2 -0
- package/dist/core-CRe_v4Mo.cjs.map +1 -0
- package/dist/core-L1AjC-fV.js +932 -0
- package/dist/core-L1AjC-fV.js.map +1 -0
- package/dist/core.js +2 -0
- package/dist/core.umd.cjs +1 -0
- package/dist/rubiks-cube-3d.js +90 -0
- package/dist/rubiks-cube-3d.js.map +1 -0
- package/dist/rubiks-cube-3d.standalone.js +4109 -0
- package/dist/rubiks-cube-3d.standalone.js.map +1 -0
- package/dist/rubiks-cube-3d.umd.cjs +2 -0
- package/dist/rubiks-cube-3d.umd.cjs.map +1 -0
- package/dist/types/RubiksCube.d.ts +36 -0
- package/dist/types/core/RubiksCubeEngine.d.ts +113 -0
- package/dist/types/core/geometry.d.ts +38 -0
- package/dist/types/core/index.d.ts +8 -0
- package/dist/types/core/mount.d.ts +26 -0
- package/dist/types/core/notation.d.ts +7 -0
- package/dist/types/core/physics.d.ts +60 -0
- package/dist/types/core/types.d.ts +150 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/standalone.d.ts +20 -0
- package/package.json +80 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});const e=require("./core-CRe_v4Mo.cjs");let t=require("react"),n=require("react/jsx-runtime");var r=typeof window<`u`?t.useLayoutEffect:t.useEffect,i=[`size`,`colors`,`sticker`,`cubieGap`,`cubieRounding`,`antialias`,`shadow`,`maxPixelRatio`],a=(0,t.forwardRef)(function({className:a,style:o,width:s=`100%`,height:c,onReady:l,...u},d){let f=(0,t.useRef)(null),p=(0,t.useRef)(null),m=(0,t.useRef)(u);m.current=u,r(()=>{let t=f.current;if(!t)return;let n={onMove:(e,t)=>m.current.onMove?.(e,t),onScrambleStart:()=>m.current.onScrambleStart?.(),onScrambleEnd:()=>m.current.onScrambleEnd?.(),onSolved:()=>m.current.onSolved?.(),onStateChange:e=>m.current.onStateChange?.(e),onHoverChange:e=>m.current.onHoverChange?.(e)},r=new e.n(t,{...m.current,...n});return p.current=r,l?.(r),()=>{r.dispose(),p.current=null}},[(0,t.useMemo)(()=>JSON.stringify(i.map(e=>u[e]??null)),i.map(e=>JSON.stringify(u[e]??null)))]);let h=JSON.stringify({background:u.background,physics:u.physics,interaction:u.interaction,camera:u.camera,exposure:u.exposure,shadowOpacity:u.shadowOpacity,moveDuration:u.moveDuration,scrambleMoves:u.scrambleMoves});(0,t.useEffect)(()=>{let e=p.current;if(!e)return;let{background:t,physics:n,interaction:r,camera:i,exposure:a,shadowOpacity:o,moveDuration:s,scrambleMoves:c}=m.current;e.setOptions({background:t,physics:n,interaction:r,camera:i,exposure:a,shadowOpacity:o,moveDuration:s,scrambleMoves:c})},[h]),(0,t.useImperativeHandle)(d,()=>({get engine(){return p.current},scramble:e=>p.current?.scramble(e)??Promise.resolve(),move:(e,t)=>p.current?.move(e,t)??Promise.resolve(),turn:(e,t)=>p.current?.turn(e,t)??Promise.resolve(),reset:()=>p.current?.reset(),resetOrientation:()=>p.current?.resetOrientation(),isSolved:()=>p.current?.isSolved()??!0,get state(){return p.current?.state??`idle`},setHover:e=>p.current?.setHover(e),setIdle:e=>p.current?.setIdle(e)}),[]);let g={position:`relative`,width:s,height:c,aspectRatio:c===void 0?`1 / 1`:void 0,...o};return(0,n.jsx)(`div`,{ref:f,className:a,style:g})});exports.CubePhysics=e.m,exports.DEFAULT_CAMERA=e.r,exports.DEFAULT_COLORS=e.i,exports.DEFAULT_INTERACTION=e.a,exports.DEFAULT_OPTIONS=e.o,exports.DEFAULT_PHYSICS=e.s,exports.DEFAULT_STICKER=e.c,exports.RubiksCube=a,exports.default=a,exports.RubiksCubeEngine=e.n,exports.createRubiksCube=e.t,exports.layerCoordinates=e.u,exports.parseNotation=e.d,exports.randomScramble=e.f,exports.resolveOptions=e.l,exports.turnToNotation=e.p;
|
|
2
|
+
//# sourceMappingURL=rubiks-cube-3d.umd.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rubiks-cube-3d.umd.cjs","names":[],"sources":["../src/RubiksCube.tsx"],"sourcesContent":["import {\n forwardRef,\n useEffect,\n useImperativeHandle,\n useLayoutEffect,\n useMemo,\n useRef,\n type CSSProperties,\n} from 'react';\nimport { RubiksCubeEngine } from './core/RubiksCubeEngine';\nimport type { CubeState } from './core/RubiksCubeEngine';\nimport type { RubiksCubeOptions, Turn } from './core/types';\n\nexport interface RubiksCubeProps extends Partial<RubiksCubeOptions> {\n className?: string;\n style?: CSSProperties;\n /** Width of the wrapper. Default '100%'. */\n width?: number | string;\n /** Height of the wrapper. Default: a square (aspect-ratio 1). */\n height?: number | string;\n /** Gets the engine instance as soon as it exists. */\n onReady?: (engine: RubiksCubeEngine) => void;\n}\n\nexport interface RubiksCubeRef {\n engine: RubiksCubeEngine | null;\n scramble(moves?: number): Promise<void>;\n move(notation: string, duration?: number): Promise<void>;\n turn(turn: Turn, duration?: number): Promise<void>;\n reset(): void;\n resetOrientation(): void;\n isSolved(): boolean;\n readonly state: CubeState;\n setHover(hovering: boolean): void;\n setIdle(enabled: boolean): void;\n}\n\nconst useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;\n\n/** Props that require the cube to be rebuilt when they change. */\nconst STRUCTURAL_KEYS = ['size', 'colors', 'sticker', 'cubieGap', 'cubieRounding', 'antialias', 'shadow', 'maxPixelRatio'] as const;\n\n/**\n * React wrapper around the three.js/Matter.js engine.\n *\n * <RubiksCube size={3} background=\"transparent\" physics={{ idleSpin: 0.4 }} />\n *\n * Structural props (size, colours, sticker shape, …) rebuild the cube; everything else is\n * updated live via `engine.setOptions`.\n */\nexport const RubiksCube = forwardRef<RubiksCubeRef, RubiksCubeProps>(function RubiksCube(\n { className, style, width = '100%', height, onReady, ...options },\n ref,\n) {\n const containerRef = useRef<HTMLDivElement>(null);\n const engineRef = useRef<RubiksCubeEngine | null>(null);\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // A stable key for the props that force a rebuild.\n const structuralKey = useMemo(\n () => JSON.stringify(STRUCTURAL_KEYS.map((k) => options[k] ?? null)),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n STRUCTURAL_KEYS.map((k) => JSON.stringify(options[k] ?? null)),\n );\n\n useIsoLayoutEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n // Callbacks are read through a ref so the latest closure is always used.\n const callbackProxy: Partial<RubiksCubeOptions> = {\n onMove: (t, n) => optionsRef.current.onMove?.(t, n),\n onScrambleStart: () => optionsRef.current.onScrambleStart?.(),\n onScrambleEnd: () => optionsRef.current.onScrambleEnd?.(),\n onSolved: () => optionsRef.current.onSolved?.(),\n onStateChange: (s) => optionsRef.current.onStateChange?.(s),\n onHoverChange: (h) => optionsRef.current.onHoverChange?.(h),\n };\n const engine = new RubiksCubeEngine(el, { ...optionsRef.current, ...callbackProxy });\n engineRef.current = engine;\n onReady?.(engine);\n return () => {\n engine.dispose();\n engineRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [structuralKey]);\n\n // Live-updatable options.\n const liveKey = JSON.stringify({\n background: options.background,\n physics: options.physics,\n interaction: options.interaction,\n camera: options.camera,\n exposure: options.exposure,\n shadowOpacity: options.shadowOpacity,\n moveDuration: options.moveDuration,\n scrambleMoves: options.scrambleMoves,\n });\n useEffect(() => {\n const engine = engineRef.current;\n if (!engine) return;\n const { background, physics, interaction, camera, exposure, shadowOpacity, moveDuration, scrambleMoves } =\n optionsRef.current;\n engine.setOptions({ background, physics, interaction, camera, exposure, shadowOpacity, moveDuration, scrambleMoves });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [liveKey]);\n\n useImperativeHandle(\n ref,\n () => ({\n get engine() {\n return engineRef.current;\n },\n scramble: (m) => engineRef.current?.scramble(m) ?? Promise.resolve(),\n move: (n, d) => engineRef.current?.move(n, d) ?? Promise.resolve(),\n turn: (t, d) => engineRef.current?.turn(t, d) ?? Promise.resolve(),\n reset: () => engineRef.current?.reset(),\n resetOrientation: () => engineRef.current?.resetOrientation(),\n isSolved: () => engineRef.current?.isSolved() ?? true,\n get state() {\n return engineRef.current?.state ?? 'idle';\n },\n setHover: (h) => engineRef.current?.setHover(h),\n setIdle: (i) => engineRef.current?.setIdle(i),\n }),\n [],\n );\n\n const wrapperStyle: CSSProperties = {\n position: 'relative',\n width,\n height,\n aspectRatio: height === undefined ? '1 / 1' : undefined,\n ...style,\n };\n\n return <div ref={containerRef} className={className} style={wrapperStyle} />;\n});\n\nexport default RubiksCube;\n"],"mappings":"4LAqCA,IAAM,EAAqB,OAAO,OAAW,IAAc,EAAA,gBAAkB,EAAA,UAGvE,EAAkB,CAAC,OAAQ,SAAU,UAAW,WAAY,gBAAiB,YAAa,SAAU,eAAe,EAU5G,GAAA,EAAa,EAAA,WAAA,CAA2C,SACnE,CAAE,YAAW,QAAO,QAAQ,OAAQ,SAAQ,UAAS,GAAG,GACxD,EACA,CACA,IAAM,GAAA,EAAe,EAAA,OAAA,CAAuB,IAAI,EAC1C,GAAA,EAAY,EAAA,OAAA,CAAgC,IAAI,EAChD,GAAA,EAAa,EAAA,OAAA,CAAO,CAAO,EACjC,EAAW,QAAU,EASrB,MAAyB,CACvB,IAAM,EAAK,EAAa,QACxB,GAAI,CAAC,EAAI,OAET,IAAM,EAA4C,CAChD,QAAS,EAAG,IAAM,EAAW,QAAQ,SAAS,EAAG,CAAC,EAClD,oBAAuB,EAAW,QAAQ,kBAAkB,EAC5D,kBAAqB,EAAW,QAAQ,gBAAgB,EACxD,aAAgB,EAAW,QAAQ,WAAW,EAC9C,cAAgB,GAAM,EAAW,QAAQ,gBAAgB,CAAC,EAC1D,cAAgB,GAAM,EAAW,QAAQ,gBAAgB,CAAC,CAC5D,EACM,EAAS,IAAI,EAAA,EAAiB,EAAI,CAAE,GAAG,EAAW,QAAS,GAAG,CAAc,CAAC,EAGnF,MAFA,GAAU,QAAU,EACpB,IAAU,CAAM,MACH,CACX,EAAO,QAAQ,EACf,EAAU,QAAU,IACtB,CAEF,EAAG,EA1BG,EAAgB,EAAA,QAAA,KACd,KAAK,UAAU,EAAgB,IAAK,GAAM,EAAQ,IAAM,IAAI,CAAC,EAEnE,EAAgB,IAAK,GAAM,KAAK,UAAU,EAAQ,IAAM,IAAI,CAAC,CAuB3D,CAAa,CAAC,EAGlB,IAAM,EAAU,KAAK,UAAU,CAC7B,WAAY,EAAQ,WACpB,QAAS,EAAQ,QACjB,YAAa,EAAQ,YACrB,OAAQ,EAAQ,OAChB,SAAU,EAAQ,SAClB,cAAe,EAAQ,cACvB,aAAc,EAAQ,aACtB,cAAe,EAAQ,aACzB,CAAC,GACD,EAAA,EAAA,UAAA,KAAgB,CACd,IAAM,EAAS,EAAU,QACzB,GAAI,CAAC,EAAQ,OACb,GAAM,CAAE,aAAY,UAAS,cAAa,SAAQ,WAAU,gBAAe,eAAc,iBACvF,EAAW,QACb,EAAO,WAAW,CAAE,aAAY,UAAS,cAAa,SAAQ,WAAU,gBAAe,eAAc,eAAc,CAAC,CAEtH,EAAG,CAAC,CAAO,CAAC,GAEZ,EAAA,EAAA,oBAAA,CACE,OACO,CACL,IAAI,QAAS,CACX,OAAO,EAAU,OACnB,EACA,SAAW,GAAM,EAAU,SAAS,SAAS,CAAC,GAAK,QAAQ,QAAQ,EACnE,MAAO,EAAG,IAAM,EAAU,SAAS,KAAK,EAAG,CAAC,GAAK,QAAQ,QAAQ,EACjE,MAAO,EAAG,IAAM,EAAU,SAAS,KAAK,EAAG,CAAC,GAAK,QAAQ,QAAQ,EACjE,UAAa,EAAU,SAAS,MAAM,EACtC,qBAAwB,EAAU,SAAS,iBAAiB,EAC5D,aAAgB,EAAU,SAAS,SAAS,GAAK,GACjD,IAAI,OAAQ,CACV,OAAO,EAAU,SAAS,OAAS,MACrC,EACA,SAAW,GAAM,EAAU,SAAS,SAAS,CAAC,EAC9C,QAAU,GAAM,EAAU,SAAS,QAAQ,CAAC,CAC9C,GACA,CAAC,CACH,EAEA,IAAM,EAA8B,CAClC,SAAU,WACV,QACA,SACA,YAAa,IAAW,IAAA,GAAY,QAAU,IAAA,GAC9C,GAAG,CACL,EAEA,OAAO,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,IAAK,EAAyB,YAAW,MAAO,CAAe,CAAA,CAC7E,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
import { RubiksCubeEngine } from './core/RubiksCubeEngine';
|
|
3
|
+
import type { CubeState } from './core/RubiksCubeEngine';
|
|
4
|
+
import type { RubiksCubeOptions, Turn } from './core/types';
|
|
5
|
+
export interface RubiksCubeProps extends Partial<RubiksCubeOptions> {
|
|
6
|
+
className?: string;
|
|
7
|
+
style?: CSSProperties;
|
|
8
|
+
/** Width of the wrapper. Default '100%'. */
|
|
9
|
+
width?: number | string;
|
|
10
|
+
/** Height of the wrapper. Default: a square (aspect-ratio 1). */
|
|
11
|
+
height?: number | string;
|
|
12
|
+
/** Gets the engine instance as soon as it exists. */
|
|
13
|
+
onReady?: (engine: RubiksCubeEngine) => void;
|
|
14
|
+
}
|
|
15
|
+
export interface RubiksCubeRef {
|
|
16
|
+
engine: RubiksCubeEngine | null;
|
|
17
|
+
scramble(moves?: number): Promise<void>;
|
|
18
|
+
move(notation: string, duration?: number): Promise<void>;
|
|
19
|
+
turn(turn: Turn, duration?: number): Promise<void>;
|
|
20
|
+
reset(): void;
|
|
21
|
+
resetOrientation(): void;
|
|
22
|
+
isSolved(): boolean;
|
|
23
|
+
readonly state: CubeState;
|
|
24
|
+
setHover(hovering: boolean): void;
|
|
25
|
+
setIdle(enabled: boolean): void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* React wrapper around the three.js/Matter.js engine.
|
|
29
|
+
*
|
|
30
|
+
* <RubiksCube size={3} background="transparent" physics={{ idleSpin: 0.4 }} />
|
|
31
|
+
*
|
|
32
|
+
* Structural props (size, colours, sticker shape, …) rebuild the cube; everything else is
|
|
33
|
+
* updated live via `engine.setOptions`.
|
|
34
|
+
*/
|
|
35
|
+
export declare const RubiksCube: import("react").ForwardRefExoticComponent<RubiksCubeProps & import("react").RefAttributes<RubiksCubeRef>>;
|
|
36
|
+
export default RubiksCube;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { type ResolvedOptions, type RubiksCubeOptions, type Turn } from './types';
|
|
2
|
+
export type CubeState = 'idle' | 'scrambling' | 'animating' | 'dragging' | 'snapping';
|
|
3
|
+
export type CubeEvent = 'move' | 'scrambleStart' | 'scrambleEnd' | 'solved' | 'stateChange' | 'hoverChange';
|
|
4
|
+
type Listener = (...args: unknown[]) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Framework-agnostic Rubik's cube renderer + mechanics. Mount it on any element:
|
|
7
|
+
*
|
|
8
|
+
* const cube = new RubiksCubeEngine(document.querySelector('#cube'), { size: 3 });
|
|
9
|
+
* cube.move("R U R' U'");
|
|
10
|
+
* cube.dispose();
|
|
11
|
+
*/
|
|
12
|
+
export declare class RubiksCubeEngine {
|
|
13
|
+
readonly container: HTMLElement;
|
|
14
|
+
readonly canvas: HTMLCanvasElement;
|
|
15
|
+
options: ResolvedOptions;
|
|
16
|
+
private renderer;
|
|
17
|
+
private scene;
|
|
18
|
+
private camera;
|
|
19
|
+
private pmrem;
|
|
20
|
+
private envTexture;
|
|
21
|
+
private keyLight;
|
|
22
|
+
private shadowPlane?;
|
|
23
|
+
/** Floats, orbits and idle-spins. Everything cube-shaped lives inside. */
|
|
24
|
+
private cubeGroup;
|
|
25
|
+
private cubies;
|
|
26
|
+
private resources?;
|
|
27
|
+
private physics;
|
|
28
|
+
private raycaster;
|
|
29
|
+
private queue;
|
|
30
|
+
private active;
|
|
31
|
+
private drag;
|
|
32
|
+
private scrambleRemaining;
|
|
33
|
+
private scrambleResolve;
|
|
34
|
+
private hovering;
|
|
35
|
+
private idleEnabled;
|
|
36
|
+
private solved;
|
|
37
|
+
private timer;
|
|
38
|
+
private elapsed;
|
|
39
|
+
private rafId;
|
|
40
|
+
private disposed;
|
|
41
|
+
private resizeObserver?;
|
|
42
|
+
private touchLeaveTimer;
|
|
43
|
+
private listeners;
|
|
44
|
+
private mountTimer;
|
|
45
|
+
constructor(container: HTMLElement, options?: Partial<RubiksCubeOptions>);
|
|
46
|
+
get state(): CubeState;
|
|
47
|
+
get isHovering(): boolean;
|
|
48
|
+
isSolved(): boolean;
|
|
49
|
+
on(event: CubeEvent, listener: Listener): () => void;
|
|
50
|
+
off(event: CubeEvent, listener: Listener): void;
|
|
51
|
+
private emit;
|
|
52
|
+
/**
|
|
53
|
+
* Scramble with the physics flywheel: slow → fast → slow.
|
|
54
|
+
* Resolves when the last move has landed.
|
|
55
|
+
*/
|
|
56
|
+
scramble(moves?: number): Promise<void>;
|
|
57
|
+
/** Apply moves in standard notation, e.g. `cube.move("R U R' U'")`. */
|
|
58
|
+
move(notation: string, duration?: number): Promise<void>;
|
|
59
|
+
/** Apply a single turn. `duration` 0 applies it instantly. */
|
|
60
|
+
turn(turn: Turn, duration?: number): Promise<void>;
|
|
61
|
+
/** Put every cubie back to the solved state immediately. */
|
|
62
|
+
reset(): void;
|
|
63
|
+
/** Programmatic hover (the same thing the mouse does). */
|
|
64
|
+
setHover(hovering: boolean): void;
|
|
65
|
+
/** Turn the idle float/spin on or off regardless of hover. */
|
|
66
|
+
setIdle(enabled: boolean): void;
|
|
67
|
+
/** Reset the cube's orientation (after orbiting / idle spinning). */
|
|
68
|
+
resetOrientation(): void;
|
|
69
|
+
/**
|
|
70
|
+
* Update options at runtime. Physics, interaction, background, exposure, shadow opacity and
|
|
71
|
+
* camera update in place; size/colours/sticker/geometry changes rebuild the cube (and reset it).
|
|
72
|
+
*/
|
|
73
|
+
setOptions(partial: Partial<RubiksCubeOptions>): void;
|
|
74
|
+
/** Tear everything down and remove the canvas. */
|
|
75
|
+
dispose(): void;
|
|
76
|
+
private applyBackground;
|
|
77
|
+
private placeCamera;
|
|
78
|
+
private buildCube;
|
|
79
|
+
private disposeCubies;
|
|
80
|
+
private buildShadow;
|
|
81
|
+
private squareMode;
|
|
82
|
+
private resize;
|
|
83
|
+
private tick;
|
|
84
|
+
private updateIdle;
|
|
85
|
+
private applyIdleState;
|
|
86
|
+
private cubiesInLayers;
|
|
87
|
+
private beginTurn;
|
|
88
|
+
private setTurnAngle;
|
|
89
|
+
/** Detach cubies from the pivot, snap them to the grid, update logical state. */
|
|
90
|
+
private completeTurn;
|
|
91
|
+
/** Cancel the active turn without applying it (used by reset/rebuild). */
|
|
92
|
+
private abortActive;
|
|
93
|
+
private applyInstant;
|
|
94
|
+
private cancelQueue;
|
|
95
|
+
private finishScramble;
|
|
96
|
+
private updateTurn;
|
|
97
|
+
private updateSolved;
|
|
98
|
+
private computeSolved;
|
|
99
|
+
private bindPointerEvents;
|
|
100
|
+
private unbindPointerEvents;
|
|
101
|
+
private preventDefault;
|
|
102
|
+
private onPointerEnter;
|
|
103
|
+
private onPointerLeave;
|
|
104
|
+
private pointerNdc;
|
|
105
|
+
private pickCubie;
|
|
106
|
+
private toScreen;
|
|
107
|
+
private onPointerDown;
|
|
108
|
+
private onPointerMove;
|
|
109
|
+
/** Pick the rotation axis + layer from the grabbed face and the drag direction. */
|
|
110
|
+
private decideLayerDrag;
|
|
111
|
+
private onPointerUp;
|
|
112
|
+
}
|
|
113
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import type { CubeColors, Face, ResolvedOptions } from './types';
|
|
3
|
+
export declare const FACE_NORMALS: Record<Face, THREE.Vector3>;
|
|
4
|
+
export interface StickerInfo {
|
|
5
|
+
face: Face;
|
|
6
|
+
/** Outward normal in the cubie's own (unrotated) frame. */
|
|
7
|
+
localNormal: THREE.Vector3;
|
|
8
|
+
mesh: THREE.Mesh;
|
|
9
|
+
}
|
|
10
|
+
export interface Cubie {
|
|
11
|
+
/** Group holding the plastic body and its stickers. Lives directly under the cube group. */
|
|
12
|
+
object: THREE.Group;
|
|
13
|
+
body: THREE.Mesh;
|
|
14
|
+
stickers: StickerInfo[];
|
|
15
|
+
/** Current logical grid position (integers for odd sizes, half-integers for even). */
|
|
16
|
+
grid: THREE.Vector3;
|
|
17
|
+
/** Grid position at solved state. */
|
|
18
|
+
home: THREE.Vector3;
|
|
19
|
+
}
|
|
20
|
+
/** Everything that has to be disposed when the cube is rebuilt. */
|
|
21
|
+
export interface CubieResources {
|
|
22
|
+
geometries: THREE.BufferGeometry[];
|
|
23
|
+
materials: THREE.Material[];
|
|
24
|
+
}
|
|
25
|
+
export declare function makeBodyMaterial(colors: CubeColors): THREE.MeshPhysicalMaterial;
|
|
26
|
+
export declare function makeStickerMaterial(color: string, roughness: number): THREE.MeshPhysicalMaterial;
|
|
27
|
+
/**
|
|
28
|
+
* Build the `size³ − (size−2)³` visible cubies. Each cubie is a rounded black box
|
|
29
|
+
* with slightly raised, rounded, glossy stickers on its exposed faces.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildCubies(opts: ResolvedOptions): {
|
|
32
|
+
cubies: Cubie[];
|
|
33
|
+
resources: CubieResources;
|
|
34
|
+
};
|
|
35
|
+
/** Round a quaternion to the nearest of the 24 cube orientations (kills drift after a turn). */
|
|
36
|
+
export declare function snapQuaternion(q: THREE.Quaternion): void;
|
|
37
|
+
/** Snap a grid coordinate to the nearest half-integer (covers odd and even sizes). */
|
|
38
|
+
export declare function snapGrid(v: THREE.Vector3): void;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { RubiksCubeEngine } from './RubiksCubeEngine';
|
|
2
|
+
export type { CubeState, CubeEvent } from './RubiksCubeEngine';
|
|
3
|
+
export { CubePhysics } from './physics';
|
|
4
|
+
export { parseNotation, turnToNotation, randomScramble, layerCoordinates } from './notation';
|
|
5
|
+
export { resolveOptions, DEFAULT_OPTIONS, DEFAULT_COLORS, DEFAULT_PHYSICS, DEFAULT_INTERACTION, DEFAULT_CAMERA, DEFAULT_STICKER, } from './types';
|
|
6
|
+
export type { Axis, Face, Turn, CubeColors, PhysicsOptions, InteractionOptions, CameraOptions, StickerOptions, RubiksCubeOptions, ResolvedOptions, } from './types';
|
|
7
|
+
export { createRubiksCube } from './mount';
|
|
8
|
+
export type { RubiksCubeHandle } from './mount';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RubiksCubeEngine, type CubeEvent, type CubeState } from './RubiksCubeEngine';
|
|
2
|
+
import type { RubiksCubeOptions, Turn } from './types';
|
|
3
|
+
/** The small, stable surface most integrations need. The full engine is on `.engine`. */
|
|
4
|
+
export interface RubiksCubeHandle {
|
|
5
|
+
engine: RubiksCubeEngine;
|
|
6
|
+
scramble(moves?: number): Promise<void>;
|
|
7
|
+
move(notation: string, duration?: number): Promise<void>;
|
|
8
|
+
turn(turn: Turn, duration?: number): Promise<void>;
|
|
9
|
+
reset(): void;
|
|
10
|
+
resetOrientation(): void;
|
|
11
|
+
isSolved(): boolean;
|
|
12
|
+
readonly state: CubeState;
|
|
13
|
+
setHover(hovering: boolean): void;
|
|
14
|
+
setIdle(enabled: boolean): void;
|
|
15
|
+
setOptions(options: Partial<RubiksCubeOptions>): void;
|
|
16
|
+
on(event: CubeEvent, listener: (...args: unknown[]) => void): () => void;
|
|
17
|
+
destroy(): void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Mount a cube into any element (framework-free).
|
|
21
|
+
*
|
|
22
|
+
* const cube = createRubiksCube(document.getElementById('cube'), { size: 3 });
|
|
23
|
+
* cube.move("R U R' U'");
|
|
24
|
+
* cube.destroy();
|
|
25
|
+
*/
|
|
26
|
+
export declare function createRubiksCube(container: HTMLElement | string, options?: Partial<RubiksCubeOptions>): RubiksCubeHandle;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Turn } from './types';
|
|
2
|
+
export declare function layerCoordinates(size: number): number[];
|
|
3
|
+
export declare function parseNotation(notation: string, size: number): Turn[];
|
|
4
|
+
/** Turn → notation string (inverse of parseNotation for common cases). */
|
|
5
|
+
export declare function turnToNotation(turn: Turn, size: number): string;
|
|
6
|
+
/** Random scramble: no two consecutive moves on the same axis+layer, no immediate undo. */
|
|
7
|
+
export declare function randomScramble(size: number, moves: number, rng?: () => number): Turn[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { PhysicsOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* All the "feel" of the cube comes from a tiny Matter.js world with three bodies:
|
|
4
|
+
*
|
|
5
|
+
* • flywheel – a mass on a frictionless rail. While the scramble spins up we push it;
|
|
6
|
+
* afterwards air friction bleeds the speed off. Its velocity IS the
|
|
7
|
+
* angular speed of the current scramble turn, which is what produces the
|
|
8
|
+
* slow → fast → slow envelope instead of a canned easing curve.
|
|
9
|
+
* • floater – a mass on a soft spring. The anchor moves between "resting" and
|
|
10
|
+
* "lifted"; a small periodic force makes it bob. Its y is the cube's
|
|
11
|
+
* hover offset.
|
|
12
|
+
* • snapper – a mass on a stiffer, under-damped spring. When you let go of a layer
|
|
13
|
+
* mid-turn its position is the layer angle and the anchor is the nearest
|
|
14
|
+
* 90°, so layers settle with a real spring wobble.
|
|
15
|
+
*/
|
|
16
|
+
export declare class CubePhysics {
|
|
17
|
+
private readonly engine;
|
|
18
|
+
private readonly flywheel;
|
|
19
|
+
private readonly floater;
|
|
20
|
+
private readonly floatSpring;
|
|
21
|
+
private readonly snapper;
|
|
22
|
+
private readonly snapSpring;
|
|
23
|
+
private opts;
|
|
24
|
+
private accumulator;
|
|
25
|
+
private elapsed;
|
|
26
|
+
private spinningUp;
|
|
27
|
+
private bobbing;
|
|
28
|
+
private snapping;
|
|
29
|
+
constructor(opts: PhysicsOptions);
|
|
30
|
+
setOptions(opts: PhysicsOptions): void;
|
|
31
|
+
/** Reset the flywheel so the next scramble starts from the slow end. */
|
|
32
|
+
resetFlywheel(): void;
|
|
33
|
+
/** While true, the flywheel is pushed each step (the "getting faster" half of a scramble). */
|
|
34
|
+
setSpinningUp(on: boolean): void;
|
|
35
|
+
/** Current scramble turn speed in rad/s, clamped to [minSpeed, maxSpeed]. */
|
|
36
|
+
get scrambleSpeed(): number;
|
|
37
|
+
/** Raw flywheel velocity (for diagnostics / tuning). */
|
|
38
|
+
get flywheelVelocity(): number;
|
|
39
|
+
/**
|
|
40
|
+
* How many quarter turns the flywheel will still travel while coasting from its
|
|
41
|
+
* current speed down to `minSpeed` under air friction. The scramble uses this to
|
|
42
|
+
* decide when to stop pushing so the last move lands right at the slow end.
|
|
43
|
+
*/
|
|
44
|
+
get quarterTurnsToCoast(): number;
|
|
45
|
+
/** Height (world units) the float spring pulls toward. */
|
|
46
|
+
setFloatTarget(y: number): void;
|
|
47
|
+
setBobbing(on: boolean): void;
|
|
48
|
+
/** Current vertical offset of the cube. */
|
|
49
|
+
get floatY(): number;
|
|
50
|
+
/** Start a spring snap from `angle` (with current angular velocity) toward `target`. */
|
|
51
|
+
beginSnap(angle: number, velocity: number, target: number): void;
|
|
52
|
+
endSnap(): void;
|
|
53
|
+
get snapAngle(): number;
|
|
54
|
+
get snapTarget(): number;
|
|
55
|
+
get snapSettled(): boolean;
|
|
56
|
+
/** Advance the world by `dtSeconds` using fixed 60 Hz sub-steps. */
|
|
57
|
+
step(dtSeconds: number): void;
|
|
58
|
+
private substep;
|
|
59
|
+
dispose(): void;
|
|
60
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** Axis of rotation in the cube's own coordinate frame. */
|
|
2
|
+
export type Axis = 'x' | 'y' | 'z';
|
|
3
|
+
/** Standard face names. U=up(+y) D=down(-y) F=front(+z) B=back(-z) L=left(-x) R=right(+x) */
|
|
4
|
+
export type Face = 'U' | 'D' | 'F' | 'B' | 'L' | 'R';
|
|
5
|
+
export interface CubeColors {
|
|
6
|
+
U: string;
|
|
7
|
+
D: string;
|
|
8
|
+
F: string;
|
|
9
|
+
B: string;
|
|
10
|
+
L: string;
|
|
11
|
+
R: string;
|
|
12
|
+
/** Plastic body colour. */
|
|
13
|
+
body: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A turn of one or more layers around one axis.
|
|
17
|
+
* `layers` are grid coordinates along `axis` (e.g. -1, 0, 1 on a 3x3; ±0.5 on a 2x2).
|
|
18
|
+
* A face move has one layer, a wide move two, a whole-cube rotation all of them.
|
|
19
|
+
*/
|
|
20
|
+
export interface Turn {
|
|
21
|
+
axis: Axis;
|
|
22
|
+
layers: number[];
|
|
23
|
+
/** Signed quarter turns. +1 = 90° counter-clockwise around the +axis (right-hand rule). */
|
|
24
|
+
quarterTurns: number;
|
|
25
|
+
}
|
|
26
|
+
export interface PhysicsOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Force applied to the scramble "flywheel" while it spins up.
|
|
29
|
+
* Bigger = the scramble reaches top speed sooner. Default 0.00066.
|
|
30
|
+
*/
|
|
31
|
+
scrambleForce: number;
|
|
32
|
+
/** Air friction on the flywheel (0..1). Controls how fast the scramble slows down. Default 0.013. */
|
|
33
|
+
scrambleFriction: number;
|
|
34
|
+
/** Top angular speed of a scramble turn, rad/s. Default 11. */
|
|
35
|
+
maxSpeed: number;
|
|
36
|
+
/** Minimum angular speed of a scramble turn, rad/s (the "slow" ends). Default 1.6. */
|
|
37
|
+
minSpeed: number;
|
|
38
|
+
/** How high the cube lifts while idle, in world units (a cubie is 1 unit). Default 0.22. */
|
|
39
|
+
floatLift: number;
|
|
40
|
+
/** Amplitude of the idle bobbing, world units. Default 0.09. */
|
|
41
|
+
floatAmplitude: number;
|
|
42
|
+
/** Bobbing frequency, Hz. Default 0.45. */
|
|
43
|
+
floatFrequency: number;
|
|
44
|
+
/** Stiffness (0..1) of the spring that carries the cube up/down between idle and hover. Default 0.004. */
|
|
45
|
+
floatStiffness: number;
|
|
46
|
+
/** Damping (0..1) of that spring. Default 0.03. */
|
|
47
|
+
floatDamping: number;
|
|
48
|
+
/** Idle spin speed around the vertical axis, rad/s. Default 0.25. */
|
|
49
|
+
idleSpin: number;
|
|
50
|
+
/** Extra idle tumble around the horizontal axis, rad/s. Default 0.06. */
|
|
51
|
+
idleTumble: number;
|
|
52
|
+
/** Stiffness (0..1) of the spring that snaps a dragged layer to the nearest 90°. Default 0.03. */
|
|
53
|
+
snapStiffness: number;
|
|
54
|
+
/** Damping (0..1) of the snap spring. Lower = more wobble. Default 0.07. */
|
|
55
|
+
snapDamping: number;
|
|
56
|
+
}
|
|
57
|
+
export interface InteractionOptions {
|
|
58
|
+
/** Master switch for pointer interaction. Default true. */
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
/** Pixels of drag for one quarter turn of a layer. Default 110. */
|
|
61
|
+
dragPixelsPerQuarterTurn: number;
|
|
62
|
+
/** Allow dragging on empty space to orbit the whole cube. Default true. */
|
|
63
|
+
orbit: boolean;
|
|
64
|
+
/** Orbit sensitivity, radians per pixel. Default 0.008. */
|
|
65
|
+
orbitSensitivity: number;
|
|
66
|
+
/** Allow the user to interrupt a running scramble by grabbing the cube. Default false. */
|
|
67
|
+
interruptScramble: boolean;
|
|
68
|
+
}
|
|
69
|
+
export interface CameraOptions {
|
|
70
|
+
/** Distance from the cube centre, in cubie units. Default 3.5 * size. */
|
|
71
|
+
distance?: number;
|
|
72
|
+
/** Vertical field of view in degrees. Default 32. */
|
|
73
|
+
fov: number;
|
|
74
|
+
/** Camera elevation above the horizon, degrees. Default 31. */
|
|
75
|
+
pitch: number;
|
|
76
|
+
/** Camera rotation around the vertical axis, degrees. Default 38. */
|
|
77
|
+
yaw: number;
|
|
78
|
+
}
|
|
79
|
+
export interface StickerOptions {
|
|
80
|
+
/** Sticker width as a fraction of the cubie. Default 0.84. */
|
|
81
|
+
inset: number;
|
|
82
|
+
/** Corner radius as a fraction of the cubie. Default 0.09. */
|
|
83
|
+
cornerRadius: number;
|
|
84
|
+
/** Sticker relief height. Default 0.018. */
|
|
85
|
+
height: number;
|
|
86
|
+
/** Roughness of the sticker vinyl (0 = mirror). Default 0.28. */
|
|
87
|
+
roughness: number;
|
|
88
|
+
}
|
|
89
|
+
export interface RubiksCubeOptions {
|
|
90
|
+
/** Cubies per edge. 2..7 supported. Default 3. */
|
|
91
|
+
size: number;
|
|
92
|
+
/** Canvas background: 'transparent' (default), any CSS colour, or a CSS gradient/image. */
|
|
93
|
+
background: string;
|
|
94
|
+
colors: Partial<CubeColors>;
|
|
95
|
+
/** Scramble the cube as soon as it is mounted. Default true. */
|
|
96
|
+
scrambleOnMount: boolean;
|
|
97
|
+
/** Number of random moves in a scramble. Default 25. */
|
|
98
|
+
scrambleMoves: number;
|
|
99
|
+
/** Delay before the mount scramble starts, ms. Default 350. */
|
|
100
|
+
scrambleDelay: number;
|
|
101
|
+
/** Duration of a programmatic `move()` in ms. Default 260. */
|
|
102
|
+
moveDuration: number;
|
|
103
|
+
physics: Partial<PhysicsOptions>;
|
|
104
|
+
interaction: Partial<InteractionOptions>;
|
|
105
|
+
camera: Partial<CameraOptions>;
|
|
106
|
+
sticker: Partial<StickerOptions>;
|
|
107
|
+
/** Soft contact shadow underneath the cube. Default true. */
|
|
108
|
+
shadow: boolean;
|
|
109
|
+
/** Shadow opacity 0..1. Default 0.28. */
|
|
110
|
+
shadowOpacity: number;
|
|
111
|
+
/** Shadow map resolution (power of two). Lower it on weak GPUs. Default 2048. */
|
|
112
|
+
shadowMapSize: number;
|
|
113
|
+
/** Cubie corner rounding, as a fraction of a cubie. Default 0.1. */
|
|
114
|
+
cubieRounding: number;
|
|
115
|
+
/** Gap between cubies, as a fraction of a cubie. Default 0.035. */
|
|
116
|
+
cubieGap: number;
|
|
117
|
+
/** Cap on device pixel ratio. Default 2. */
|
|
118
|
+
maxPixelRatio: number;
|
|
119
|
+
antialias: boolean;
|
|
120
|
+
/** Overall brightness of the reflections/lighting. Default 1. */
|
|
121
|
+
exposure: number;
|
|
122
|
+
/** Start floating/spinning even before the first mouse-leave. Default true. */
|
|
123
|
+
idleOnMount: boolean;
|
|
124
|
+
/** Fired after every completed turn (user drag, scramble, or API). */
|
|
125
|
+
onMove?: (turn: Turn, notation: string) => void;
|
|
126
|
+
onScrambleStart?: () => void;
|
|
127
|
+
onScrambleEnd?: () => void;
|
|
128
|
+
/** Fired when the cube becomes solved after a turn. */
|
|
129
|
+
onSolved?: () => void;
|
|
130
|
+
/** Fired when the cube state changes; `solved` is the current state. */
|
|
131
|
+
onStateChange?: (solved: boolean) => void;
|
|
132
|
+
onHoverChange?: (hovering: boolean) => void;
|
|
133
|
+
}
|
|
134
|
+
export declare const DEFAULT_COLORS: CubeColors;
|
|
135
|
+
export declare const DEFAULT_PHYSICS: PhysicsOptions;
|
|
136
|
+
export declare const DEFAULT_INTERACTION: InteractionOptions;
|
|
137
|
+
export declare const DEFAULT_CAMERA: CameraOptions;
|
|
138
|
+
export declare const DEFAULT_STICKER: StickerOptions;
|
|
139
|
+
export declare const DEFAULT_OPTIONS: RubiksCubeOptions;
|
|
140
|
+
/** Fully resolved options with every default applied. */
|
|
141
|
+
export interface ResolvedOptions extends Omit<RubiksCubeOptions, 'colors' | 'physics' | 'interaction' | 'camera' | 'sticker'> {
|
|
142
|
+
colors: CubeColors;
|
|
143
|
+
physics: PhysicsOptions;
|
|
144
|
+
interaction: InteractionOptions;
|
|
145
|
+
camera: CameraOptions;
|
|
146
|
+
sticker: StickerOptions;
|
|
147
|
+
}
|
|
148
|
+
/** Drop `undefined` values so they never override a default (React passes unset props as undefined). */
|
|
149
|
+
export declare function compact<T extends object>(obj: T | undefined): Partial<T>;
|
|
150
|
+
export declare function resolveOptions(user?: Partial<RubiksCubeOptions>): ResolvedOptions;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone build — no React, three.js and Matter.js bundled in.
|
|
3
|
+
*
|
|
4
|
+
* <script src="https://unpkg.com/rubiks-cube-3d/dist/rubiks-cube-3d.standalone.js"></script>
|
|
5
|
+
*
|
|
6
|
+
* // 1. declarative: any element with data-rubiks-cube is mounted automatically
|
|
7
|
+
* <div data-rubiks-cube data-size="3" data-background="transparent"
|
|
8
|
+
* data-options='{"physics":{"idleSpin":0.4}}' style="width:320px"></div>
|
|
9
|
+
*
|
|
10
|
+
* // 2. imperative
|
|
11
|
+
* const cube = RubiksCube3D.mount('#cube', { size: 4 });
|
|
12
|
+
* cube.move("R U R' U'");
|
|
13
|
+
*/
|
|
14
|
+
import { createRubiksCube, type RubiksCubeHandle } from './core/mount';
|
|
15
|
+
export * from './core';
|
|
16
|
+
export { createRubiksCube as mount };
|
|
17
|
+
/** Mount every `[data-rubiks-cube]` element that is not mounted yet. */
|
|
18
|
+
export declare function autoMount(root?: ParentNode): RubiksCubeHandle[];
|
|
19
|
+
/** Get the handle for an auto-mounted element. */
|
|
20
|
+
export declare function get(el: Element): RubiksCubeHandle | undefined;
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rubiks-cube-3d",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A realistic, physics-driven, interactive 3D Rubik's cube built with three.js and Matter.js. Ships as a React component and as a standalone script for any website.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"rubiks",
|
|
9
|
+
"cube",
|
|
10
|
+
"three",
|
|
11
|
+
"threejs",
|
|
12
|
+
"matter-js",
|
|
13
|
+
"react",
|
|
14
|
+
"3d",
|
|
15
|
+
"physics",
|
|
16
|
+
"interactive"
|
|
17
|
+
],
|
|
18
|
+
"main": "./dist/rubiks-cube-3d.umd.cjs",
|
|
19
|
+
"module": "./dist/rubiks-cube-3d.js",
|
|
20
|
+
"types": "./dist/types/index.d.ts",
|
|
21
|
+
"unpkg": "./dist/rubiks-cube-3d.standalone.js",
|
|
22
|
+
"jsdelivr": "./dist/rubiks-cube-3d.standalone.js",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/types/index.d.ts",
|
|
26
|
+
"import": "./dist/rubiks-cube-3d.js",
|
|
27
|
+
"require": "./dist/rubiks-cube-3d.umd.cjs"
|
|
28
|
+
},
|
|
29
|
+
"./core": {
|
|
30
|
+
"types": "./dist/types/core/index.d.ts",
|
|
31
|
+
"import": "./dist/core.js"
|
|
32
|
+
},
|
|
33
|
+
"./standalone": "./dist/rubiks-cube-3d.standalone.js",
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"sideEffects": [
|
|
41
|
+
"./dist/rubiks-cube-3d.standalone.js"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"dev": "vite --config vite.demo.config.ts",
|
|
45
|
+
"build": "npm run build:lib && npm run build:standalone && npm run build:types",
|
|
46
|
+
"build:lib": "BUILD_TARGET=lib vite build",
|
|
47
|
+
"build:standalone": "BUILD_TARGET=standalone vite build",
|
|
48
|
+
"build:types": "tsc -p tsconfig.build.json",
|
|
49
|
+
"build:demo": "vite build --config vite.demo.config.ts",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"react": ">=17",
|
|
54
|
+
"react-dom": ">=17"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"react": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"react-dom": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"matter-js": "^0.20.0",
|
|
66
|
+
"three": "^0.185.1"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/matter-js": "^0.20.2",
|
|
70
|
+
"@types/node": "^22.20.1",
|
|
71
|
+
"@types/react": "^19.1.0",
|
|
72
|
+
"@types/react-dom": "^19.1.0",
|
|
73
|
+
"@types/three": "^0.185.0",
|
|
74
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
75
|
+
"react": "^19.1.0",
|
|
76
|
+
"react-dom": "^19.1.0",
|
|
77
|
+
"typescript": "^5.9.0",
|
|
78
|
+
"vite": "^8.2.2"
|
|
79
|
+
}
|
|
80
|
+
}
|