diff --git a/package-lock.json b/package-lock.json index 3b9b538..c8e9b0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@planara/react", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@planara/react", - "version": "1.1.0", + "version": "1.2.0", "dependencies": { - "@planara/core": "^3.1.0", - "@planara/types": "^1.12.0" + "@planara/core": "^3.2.1", + "@planara/types": "^1.13.0" }, "devDependencies": { "@eslint/js": "^9.36.0", @@ -1207,13 +1207,13 @@ } }, "node_modules/@planara/core": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@planara/core/-/core-3.1.0.tgz", - "integrity": "sha512-Znsx3BCgGDIoK0HTY44a4QbaFxIfilT6VkyPE04xXul75qH9os2CP8KbadDIMvt/dleYTjfG6KhXTl65nxZc5Q==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@planara/core/-/core-3.2.1.tgz", + "integrity": "sha512-4qLAEIzdnbHJgtsernTVJ4IQ0PK18A91gZUuASeygvG7OG4ccicx6SQc/xnyT1IKWgn3VguV9ia+iWE+MwH3ig==", "license": "MIT", "dependencies": { - "@planara/three": "^1.4.3", - "@planara/types": "^1.12.0", + "@planara/three": "^1.5.0", + "@planara/types": "^1.13.0", "events": "^3.3.0", "mobx": "^6.15.0", "reflect-metadata": "^0.2.2", @@ -1225,9 +1225,9 @@ } }, "node_modules/@planara/three": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@planara/three/-/three-1.4.3.tgz", - "integrity": "sha512-pRjQcKcKzDGhi5qk7WkTrOBYc6tamkwq0Xlgc1wGAL+/4eSe8OByvAgeJJJ0SIEVJYZwD4lUV1iCcp0n7RJccg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@planara/three/-/three-1.5.0.tgz", + "integrity": "sha512-J3L//G7XqqFnZ1F+DgNaSuxRbRD0GyPgOEgZqdq++ItrmGBnYAPHGtTPXN0Ib5mH3bH/zxzVwuNwn/nsMmUZ0A==", "license": "MIT", "peerDependencies": { "@types/three": "^0.181.0", @@ -1235,9 +1235,9 @@ } }, "node_modules/@planara/types": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@planara/types/-/types-1.12.0.tgz", - "integrity": "sha512-obp1lHF2wdFIEObzciF5+uJ92YmB3qY/qoxzczuYw3yEn1uhsWCtv9IaZr+zJ8HcsDhEINY0IFMNi9a8hZs9hA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@planara/types/-/types-1.13.0.tgz", + "integrity": "sha512-TQPKefqjV4jh6YenGsH1Wcb2O+qFOKRKueNQyL+qeWBgKspIZC0gI0eej00hBqCpQyGxa+PqmpGLCURmttoQYw==", "license": "MIT" }, "node_modules/@rolldown/pluginutils": { @@ -2302,9 +2302,9 @@ "license": "MIT" }, "node_modules/@webgpu/types": { - "version": "0.1.69", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", - "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.70.tgz", + "integrity": "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==", "license": "BSD-3-Clause", "peer": true }, @@ -3068,9 +3068,9 @@ } }, "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "license": "MIT", "peer": true }, diff --git a/package.json b/package.json index 5343d44..a1e28c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@planara/react", - "version": "1.2.0", + "version": "1.3.0", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" @@ -20,6 +20,14 @@ "files": [ "dist" ], + "keywords": [ + "planara", + "3d", + "react" + ], + "author": "a1unade", + "license": "MIT", + "description": "React adapter for Planara", "scripts": { "dev": "vite", "build": "tsc -b && vite build", @@ -30,8 +38,8 @@ "check-format": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"" }, "dependencies": { - "@planara/core": "^3.1.0", - "@planara/types": "^1.12.0" + "@planara/core": "^3.2.1", + "@planara/types": "^1.13.0" }, "devDependencies": { "@eslint/js": "^9.36.0", @@ -55,5 +63,9 @@ "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/planara/planara-react.git" } } diff --git a/src/components/canvas/benchmark-canvas.tsx b/src/components/canvas/benchmark-canvas.tsx new file mode 100644 index 0000000..c7cc57e --- /dev/null +++ b/src/components/canvas/benchmark-canvas.tsx @@ -0,0 +1,75 @@ +import React, { useEffect, useRef } from 'react'; +// Core +import { createBenchmarkHub } from '@planara/core'; +// Interfaces +import type { IBenchmarkApi } from '../../interfaces/benchmark-api'; +// Contexts +import { useBenchmarkHubContext } from '../../contexts/benchmark-hub-context'; +// Types +import type { RendererConfigInput } from '@planara/types'; + +interface BenchmarkCanvasProps { + className?: string; + width?: number; + height?: number; + config?: RendererConfigInput; +} + +export const BenchmarkCanvas: React.FC = ({ + className, + width = 1000, + height = 1000, + config, +}) => { + const canvasRef = useRef(null); + const { setHub } = useBenchmarkHubContext(); + + useEffect(() => { + if (!canvasRef.current) return; + + const canvas = canvasRef.current; + const parent = canvas.parentElement; + if (!parent) return; + + const benchmarkHub = createBenchmarkHub(canvas, config); + + const api: IBenchmarkApi = { + run: (benchmarkConfig) => benchmarkHub.run(benchmarkConfig), + getReport: () => benchmarkHub.getReport(), + subscribeMetrics: (listener) => benchmarkHub.subscribeMetrics(listener), + + resizeRenderer: () => benchmarkHub.resizeRenderer(), + start: () => benchmarkHub.start(), + stop: () => benchmarkHub.stop(), + }; + + setHub(api); + + const handleResize = () => { + const nextWidth = parent.clientWidth; + const nextHeight = parent.clientHeight; + + canvas.style.width = `${nextWidth}px`; + canvas.style.height = `${nextHeight}px`; + + api.resizeRenderer(); + }; + + handleResize(); + api.start(); + + window.addEventListener('resize', handleResize); + + return () => { + api.stop(); + window.removeEventListener('resize', handleResize); + + benchmarkHub.dispose(); + setHub(null); + }; + }, [setHub, config]); + + return ; +}; + +export default BenchmarkCanvas; diff --git a/src/contexts/benchmark-hub-context.ts b/src/contexts/benchmark-hub-context.ts new file mode 100644 index 0000000..c4b6736 --- /dev/null +++ b/src/contexts/benchmark-hub-context.ts @@ -0,0 +1,24 @@ +import { createContext, useContext } from 'react'; +// Interfaces +import type { IBenchmarkApi } from '../interfaces/benchmark-api'; + +export interface IBenchmarkContext { + hub: IBenchmarkApi | null; + setHub: (hub: IBenchmarkApi | null) => void; +} + +export const BenchmarkHubContext = createContext(null); + +export const useBenchmarkHubContext = (): IBenchmarkContext => { + const context = useContext(BenchmarkHubContext); + + if (!context) { + throw new Error('useBenchmarkHubContext must be used inside BenchmarkProvider'); + } + + return context; +}; + +export const useBenchmarkHub = (): IBenchmarkApi | null => { + return useBenchmarkHubContext().hub; +}; diff --git a/src/hooks/use-benchmark-metrics.ts b/src/hooks/use-benchmark-metrics.ts new file mode 100644 index 0000000..dd9b1f9 --- /dev/null +++ b/src/hooks/use-benchmark-metrics.ts @@ -0,0 +1,24 @@ +// Core +import { useEffect, useState } from 'react'; +// Types +import type { BenchmarkMetrics } from '@planara/types'; +// Contexts +import { useBenchmarkHub } from '../contexts/benchmark-hub-context'; + +export const useBenchmarkMetrics = (): BenchmarkMetrics | null => { + const hub = useBenchmarkHub(); + const [metrics, setMetrics] = useState(null); + + useEffect(() => { + if (!hub) { + setMetrics(null); + return; + } + + return hub.subscribeMetrics((nextMetrics) => { + setMetrics(nextMetrics); + }); + }, [hub]); + + return metrics; +}; diff --git a/src/index.ts b/src/index.ts index f92db2f..cd333b0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,3 +10,9 @@ export { EditorCanvas } from './components/canvas/editor-canvas'; export { useEditorHub, useEditorHubContext } from './contexts/editor-hub-context'; export { useSelectionStats } from './hooks/use-selection-stats'; export { makeEditorHandlers } from './actions/button'; + +// Benchmark +export { BenchmarkProvider } from './providers/benchmark-provider'; +export { BenchmarkCanvas } from './components/canvas/benchmark-canvas'; +export { useBenchmarkHub, useBenchmarkHubContext } from './contexts/benchmark-hub-context'; +export { useBenchmarkMetrics } from './hooks/use-benchmark-metrics'; diff --git a/src/interfaces/benchmark-api.ts b/src/interfaces/benchmark-api.ts new file mode 100644 index 0000000..6e67891 --- /dev/null +++ b/src/interfaces/benchmark-api.ts @@ -0,0 +1,20 @@ +import type { + BenchmarkConfig, + BenchmarkReport, + BenchmarkRunResult, + BenchmarkMetrics, +} from '@planara/types'; + +export type MetricsListener = (metrics: BenchmarkMetrics | null) => void; + +export interface IBenchmarkApi { + run: (config: BenchmarkConfig) => BenchmarkRunResult; + + getReport: () => BenchmarkReport; + + subscribeMetrics: (listener: MetricsListener) => () => void; + + resizeRenderer: () => void; + start: () => void; + stop: () => void; +} diff --git a/src/providers/benchmark-provider.tsx b/src/providers/benchmark-provider.tsx new file mode 100644 index 0000000..b8ed7aa --- /dev/null +++ b/src/providers/benchmark-provider.tsx @@ -0,0 +1,26 @@ +// Core +import React, { useMemo, useState } from 'react'; +// Interfaces +import type { IBenchmarkApi } from '../interfaces/benchmark-api'; +// Contexts +import { BenchmarkHubContext } from '../contexts/benchmark-hub-context'; + +interface BenchmarkProviderProps { + children: React.ReactNode; +} + +export const BenchmarkProvider: React.FC = ({ children }) => { + const [hub, setHub] = useState(null); + + const value = useMemo( + () => ({ + hub, + setHub, + }), + [hub], + ); + + return {children}; +}; + +export default BenchmarkProvider;