@decido/plugin-planta-erp 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.
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Planta ERP</title>
7
+ <script type="module" crossorigin src="http://localhost:5004/assets/index-YxWqJStM.js"></script>
8
+ <link rel="stylesheet" crossorigin href="http://localhost:5004/assets/style.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "plugin-planta-erp",
3
+ "name": "Planta ERP",
4
+ "description": "Sistema Operativo de Producción y Órdenes",
5
+ "icon": "factory",
6
+ "widgets": [
7
+ {
8
+ "id": "ws-digital-twin",
9
+ "name": "Digital Twin - Planta",
10
+ "defaultZone": "main-canvas"
11
+ }
12
+ ],
13
+ "iframeUrls": {
14
+ "ws-digital-twin": "http://localhost:5004/"
15
+ }
16
+ }
@@ -0,0 +1,95 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src-backend/index.ts
20
+ var index_exports = {};
21
+ __export(index_exports, {
22
+ createPlantaErpServerPlugin: () => createPlantaErpServerPlugin
23
+ });
24
+ module.exports = __toCommonJS(index_exports);
25
+
26
+ // src-backend/migrations.ts
27
+ var plantaMigrations = [
28
+ {
29
+ name: "001_init_planta_schema",
30
+ sql: `
31
+ -- 1. Create Schema
32
+ CREATE SCHEMA IF NOT EXISTS planta;
33
+
34
+ -- 2. Create Base Machine Nodes Table (Industrial Property)
35
+ CREATE TABLE IF NOT EXISTS planta.machine_nodes (
36
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
37
+ tenant_id UUID NOT NULL,
38
+ name VARCHAR(255) NOT NULL,
39
+ node_type VARCHAR(100) NOT NULL,
40
+ metadata JSONB DEFAULT '{}',
41
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
42
+ );
43
+
44
+ -- 3. Enforce RLS (Zero-Trust Security)
45
+ ALTER TABLE planta.machine_nodes ENABLE ROW LEVEL SECURITY;
46
+
47
+ DO $$
48
+ BEGIN
49
+ IF NOT EXISTS (
50
+ SELECT 1 FROM pg_policies
51
+ WHERE schemaname = 'planta' AND tablename = 'machine_nodes' AND policyname = 'tenant_isolation_policy'
52
+ ) THEN
53
+ CREATE POLICY tenant_isolation_policy ON planta.machine_nodes
54
+ USING (tenant_id = nullif(current_setting('app.current_tenant', true), '')::uuid);
55
+ END IF;
56
+ END $$;
57
+ `
58
+ }
59
+ ];
60
+
61
+ // src-backend/index.ts
62
+ var createPlantaErpServerPlugin = (deps) => ({
63
+ name: "planta_erp_backend",
64
+ mountRoutes: (apiRouter) => {
65
+ apiRouter.use("/twin", deps.digitalTwinRouter);
66
+ apiRouter.use("/simulation", deps.simulationRouter);
67
+ apiRouter.use("/scenarios", deps.simulationStudioRouter);
68
+ apiRouter.use("/simulation/scenarios", deps.simulationStudioRouter);
69
+ apiRouter.use("/track", deps.trackingRouter);
70
+ apiRouter.use("/whatsapp", deps.whatsappAutomationRouter);
71
+ apiRouter.use("/webhooks/whatsapp", deps.whatsappWebhookRouter);
72
+ },
73
+ mountSockets: (io) => {
74
+ if (deps.initSocketsHook) {
75
+ deps.initSocketsHook(io);
76
+ }
77
+ },
78
+ initServices: async () => {
79
+ console.log("[PlantaErpServerPlugin] Initializing Industrial services...");
80
+ if (deps.initServicesHook) {
81
+ await deps.initServicesHook();
82
+ }
83
+ },
84
+ getMigrations: async () => {
85
+ return {
86
+ pluginName: "planta_erp_backend",
87
+ schemaName: "planta",
88
+ migrations: plantaMigrations
89
+ };
90
+ }
91
+ });
92
+ // Annotate the CommonJS export names for ESM import in node:
93
+ 0 && (module.exports = {
94
+ createPlantaErpServerPlugin
95
+ });
@@ -0,0 +1,25 @@
1
+ import { Router } from 'express';
2
+ import { Server } from 'socket.io';
3
+ import { IServerPlugin } from '@decido/sdk';
4
+
5
+ /**
6
+ * Dependencias físicas de la industria privada proporcionadas
7
+ * por el Host para inyección y enrutamiento en el Gemelo Digital.
8
+ */
9
+ interface PlantaErpBackendDeps {
10
+ digitalTwinRouter: Router;
11
+ simulationRouter: Router;
12
+ simulationStudioRouter: Router;
13
+ trackingRouter: Router;
14
+ whatsappAutomationRouter: Router;
15
+ whatsappWebhookRouter: Router;
16
+ initServicesHook?: () => Promise<void>;
17
+ initSocketsHook?: (io: Server) => void;
18
+ }
19
+ /**
20
+ * Crea la Extensión B2B para Planta Industria, conectando
21
+ * el Engine de Renderizado, Simulaciones y Automatización WA.
22
+ */
23
+ declare const createPlantaErpServerPlugin: (deps: PlantaErpBackendDeps) => IServerPlugin;
24
+
25
+ export { type PlantaErpBackendDeps, createPlantaErpServerPlugin };
@@ -0,0 +1,25 @@
1
+ import { Router } from 'express';
2
+ import { Server } from 'socket.io';
3
+ import { IServerPlugin } from '@decido/sdk';
4
+
5
+ /**
6
+ * Dependencias físicas de la industria privada proporcionadas
7
+ * por el Host para inyección y enrutamiento en el Gemelo Digital.
8
+ */
9
+ interface PlantaErpBackendDeps {
10
+ digitalTwinRouter: Router;
11
+ simulationRouter: Router;
12
+ simulationStudioRouter: Router;
13
+ trackingRouter: Router;
14
+ whatsappAutomationRouter: Router;
15
+ whatsappWebhookRouter: Router;
16
+ initServicesHook?: () => Promise<void>;
17
+ initSocketsHook?: (io: Server) => void;
18
+ }
19
+ /**
20
+ * Crea la Extensión B2B para Planta Industria, conectando
21
+ * el Engine de Renderizado, Simulaciones y Automatización WA.
22
+ */
23
+ declare const createPlantaErpServerPlugin: (deps: PlantaErpBackendDeps) => IServerPlugin;
24
+
25
+ export { type PlantaErpBackendDeps, createPlantaErpServerPlugin };
@@ -0,0 +1,69 @@
1
+ // src-backend/migrations.ts
2
+ var plantaMigrations = [
3
+ {
4
+ name: "001_init_planta_schema",
5
+ sql: `
6
+ -- 1. Create Schema
7
+ CREATE SCHEMA IF NOT EXISTS planta;
8
+
9
+ -- 2. Create Base Machine Nodes Table (Industrial Property)
10
+ CREATE TABLE IF NOT EXISTS planta.machine_nodes (
11
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
12
+ tenant_id UUID NOT NULL,
13
+ name VARCHAR(255) NOT NULL,
14
+ node_type VARCHAR(100) NOT NULL,
15
+ metadata JSONB DEFAULT '{}',
16
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
17
+ );
18
+
19
+ -- 3. Enforce RLS (Zero-Trust Security)
20
+ ALTER TABLE planta.machine_nodes ENABLE ROW LEVEL SECURITY;
21
+
22
+ DO $$
23
+ BEGIN
24
+ IF NOT EXISTS (
25
+ SELECT 1 FROM pg_policies
26
+ WHERE schemaname = 'planta' AND tablename = 'machine_nodes' AND policyname = 'tenant_isolation_policy'
27
+ ) THEN
28
+ CREATE POLICY tenant_isolation_policy ON planta.machine_nodes
29
+ USING (tenant_id = nullif(current_setting('app.current_tenant', true), '')::uuid);
30
+ END IF;
31
+ END $$;
32
+ `
33
+ }
34
+ ];
35
+
36
+ // src-backend/index.ts
37
+ var createPlantaErpServerPlugin = (deps) => ({
38
+ name: "planta_erp_backend",
39
+ mountRoutes: (apiRouter) => {
40
+ apiRouter.use("/twin", deps.digitalTwinRouter);
41
+ apiRouter.use("/simulation", deps.simulationRouter);
42
+ apiRouter.use("/scenarios", deps.simulationStudioRouter);
43
+ apiRouter.use("/simulation/scenarios", deps.simulationStudioRouter);
44
+ apiRouter.use("/track", deps.trackingRouter);
45
+ apiRouter.use("/whatsapp", deps.whatsappAutomationRouter);
46
+ apiRouter.use("/webhooks/whatsapp", deps.whatsappWebhookRouter);
47
+ },
48
+ mountSockets: (io) => {
49
+ if (deps.initSocketsHook) {
50
+ deps.initSocketsHook(io);
51
+ }
52
+ },
53
+ initServices: async () => {
54
+ console.log("[PlantaErpServerPlugin] Initializing Industrial services...");
55
+ if (deps.initServicesHook) {
56
+ await deps.initServicesHook();
57
+ }
58
+ },
59
+ getMigrations: async () => {
60
+ return {
61
+ pluginName: "planta_erp_backend",
62
+ schemaName: "planta",
63
+ migrations: plantaMigrations
64
+ };
65
+ }
66
+ });
67
+ export {
68
+ createPlantaErpServerPlugin
69
+ };
package/index.html ADDED
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Planta ERP</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/dev-mount.tsx"></script>
11
+ </body>
12
+ </html>
package/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import { CortexManifest } from '@decido/sdk';
3
+
4
+ const DigitalTwinWidget = React.lazy(() => import('./src/components/DigitalTwin/PhysicalCanvas').then(m => ({ default: m.PhysicalCanvas })));
5
+
6
+ const PlantaErpPlugin: CortexManifest = {
7
+ id: 'plugin-planta-erp',
8
+ name: 'Planta ERP',
9
+ description: 'Sistema Operativo de Producción y Órdenes',
10
+ author: 'Decido',
11
+ version: '1.0.0',
12
+ icon: 'factory',
13
+ engines: {},
14
+ widgets: [
15
+ {
16
+ id: 'ws-digital-twin',
17
+ name: 'Digital Twin - Planta',
18
+ component: DigitalTwinWidget,
19
+ defaultZone: 'main-canvas'
20
+ }
21
+ ],
22
+ permissions: [],
23
+ intents: []
24
+ };
25
+
26
+ export default PlantaErpPlugin;
package/manifest.ts ADDED
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { CortexManifest } from '@decido/sdk';
3
+
4
+ const DigitalTwinWidget = React.lazy(() => import('./src/components/DigitalTwin/PhysicalCanvas').then(m => ({ default: m.PhysicalCanvas })));
5
+
6
+ export const PlantaErpPlugin: CortexManifest = {
7
+ id: 'plugin-planta-erp',
8
+ name: 'Planta ERP',
9
+ description: 'Sistema Operativo de Producción y Órdenes',
10
+ author: 'Decido',
11
+ version: '1.0.0',
12
+ icon: 'factory',
13
+ engines: {},
14
+ widgets: [
15
+ {
16
+ id: 'ws-digital-twin',
17
+ name: 'Digital Twin - Planta',
18
+ component: DigitalTwinWidget,
19
+ defaultZone: 'main-canvas'
20
+ }
21
+ ],
22
+ permissions: [],
23
+ intents: []
24
+ };
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@decido/plugin-planta-erp",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./server": {
13
+ "types": "./dist-server/index.d.ts",
14
+ "import": "./dist-server/index.js",
15
+ "require": "./dist-server/index.cjs"
16
+ }
17
+ },
18
+ "dependencies": {
19
+ "@dnd-kit/core": "^6.1.0",
20
+ "@dnd-kit/sortable": "^8.0.0",
21
+ "@dnd-kit/utilities": "^3.2.2",
22
+ "@react-three/drei": "^10.7.7",
23
+ "@react-three/fiber": "^9.5.0",
24
+ "@react-three/rapier": "^2.2.0",
25
+ "@types/three": "^0.165.0",
26
+ "@xyflow/react": "^12.0.0",
27
+ "framer-motion": "^11.0.0",
28
+ "lucide-react": "^0.300.0",
29
+ "sonner": "^1.4.3",
30
+ "three": "^0.183.1",
31
+ "y-indexeddb": "^9.0.12",
32
+ "y-websocket": "^3.0.0",
33
+ "yjs": "^13.6.29",
34
+ "zustand": "4.5.2",
35
+ "@decido/ui-kit": "^2.0.0"
36
+ },
37
+ "peerDependencies": {
38
+ "react": "^18.2.0",
39
+ "react-dom": "^18.2.0",
40
+ "@decido/shell": "1.0.0",
41
+ "@decido/canvas-core": "0.1.0",
42
+ "@decido/sdk": "1.0.0",
43
+ "@decido/kernel-bridge": "1.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@originjs/vite-plugin-federation": "^1.4.1",
47
+ "@tailwindcss/vite": "^4.2.1",
48
+ "@types/express": "^5.0.6",
49
+ "@types/socket.io": "^3.0.2",
50
+ "@vitejs/plugin-react": "^4.2.1",
51
+ "express": "^5.2.1",
52
+ "react": "^18.3.1",
53
+ "react-dom": "^18.3.1",
54
+ "socket.io": "^4.8.3",
55
+ "tailwindcss": "^4.2.1",
56
+ "tsup": "^8.5.1",
57
+ "typescript": "^5.7.3",
58
+ "vite": "^5.2.0"
59
+ },
60
+ "license": "UNLICENSED",
61
+ "scripts": {
62
+ "dev": "kill-port 5004 || true && pnpm run build && pnpm run preview",
63
+ "build": "vite build && pnpm run build:server",
64
+ "build:server": "tsup src-backend/index.ts --format cjs,esm --dts --outDir dist-server",
65
+ "preview": "vite preview --port 5004 --strictPort"
66
+ }
67
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "plugin-planta-erp",
3
+ "name": "Planta ERP",
4
+ "description": "Sistema Operativo de Producción y Órdenes",
5
+ "icon": "factory",
6
+ "widgets": [
7
+ {
8
+ "id": "ws-digital-twin",
9
+ "name": "Digital Twin - Planta",
10
+ "defaultZone": "main-canvas"
11
+ }
12
+ ],
13
+ "iframeUrls": {
14
+ "ws-digital-twin": "http://localhost:5004/"
15
+ }
16
+ }
@@ -0,0 +1 @@
1
+ export * from "../dist-server/index";
@@ -0,0 +1 @@
1
+ module.exports = require('../dist-server/index.cjs');
@@ -0,0 +1,4 @@
1
+ {
2
+ "main": "index.js",
3
+ "types": "index.d.ts"
4
+ }
@@ -0,0 +1,80 @@
1
+ import React, { Suspense } from 'react';
2
+ import { Canvas } from '@react-three/fiber';
3
+ import { Physics, RigidBody } from '@react-three/rapier';
4
+ import { OrbitControls, Environment, ContactShadows } from '@react-three/drei';
5
+ import { useDigitalTwinStore } from '../../stores/digitalTwinStore';
6
+ import { MachineNode3D } from './nodes/MachineNode3D';
7
+ import { OrderProduct3D } from './nodes/OrderProduct3D';
8
+
9
+ export const PhysicalCanvas: React.FC = () => {
10
+ const nodes = useDigitalTwinStore(state => state.nodes);
11
+
12
+ return (
13
+ <div className="w-full h-full bg-neutral-900 rounded-2xl overflow-hidden relative">
14
+ <div className="absolute top-4 left-4 z-10 bg-black/80 backdrop-blur-md px-4 py-3 rounded-lg border border-orange-500/30">
15
+ <h3 className="text-white font-bold text-lg flex items-center gap-2">
16
+ <span className="w-2 h-2 rounded-full bg-orange-500 animate-ping"></span>
17
+ Digital Twin (Rapier Physics)
18
+ </h3>
19
+ <p className="text-xs text-neutral-400 mt-1">Spatial Engine Active. Click blocks to apply forces.</p>
20
+ </div>
21
+
22
+ <Canvas shadows camera={{ position: [0, 8, 12], fov: 45 }}>
23
+ {/* Iluminación y Ambiente */}
24
+ <ambientLight intensity={0.4} />
25
+ <directionalLight
26
+ castShadow
27
+ position={[10, 20, 10]}
28
+ intensity={1.5}
29
+ shadow-mapSize={[1024, 1024]}
30
+ />
31
+
32
+ <Suspense fallback={null}>
33
+ <Environment preset="warehouse" background blur={0.8} />
34
+ </Suspense>
35
+
36
+ {/* Controles de cámara */}
37
+ <OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} />
38
+
39
+ {/* Motor Físico */}
40
+ <Physics gravity={[0, -9.81, 0]}>
41
+ {/* Piso Estático (Suelo de la fábrica) */}
42
+ <RigidBody type="fixed" colliders="cuboid" restitution={0.2} friction={1}>
43
+ <mesh position={[0, -0.5, 0]} receiveShadow>
44
+ <boxGeometry args={[40, 1, 40]} />
45
+ <meshStandardMaterial color="#171717" roughness={0.9} />
46
+ </mesh>
47
+ </RigidBody>
48
+
49
+ {/* Sombras de Contacto Suaves */}
50
+ <ContactShadows position={[0, 0.01, 0]} scale={20} blur={2} opacity={0.4} far={10} />
51
+
52
+ {/* Rederizado Dinámico de Nodos (Máquinas y Productos) */}
53
+ {Object.values(nodes).map(node => {
54
+ if (node.type === 'machine') {
55
+ return (
56
+ <MachineNode3D
57
+ key={node.id}
58
+ id={node.id}
59
+ position={node.position}
60
+ state={node.state || 'idle'}
61
+ metadata={node.metadata}
62
+ />
63
+ );
64
+ } else if (node.type === 'order') {
65
+ return (
66
+ <OrderProduct3D
67
+ key={node.id}
68
+ id={node.id}
69
+ position={node.position}
70
+ metadata={node.metadata}
71
+ />
72
+ );
73
+ }
74
+ return null;
75
+ })}
76
+ </Physics>
77
+ </Canvas>
78
+ </div>
79
+ );
80
+ };
@@ -0,0 +1,61 @@
1
+ import React from 'react';
2
+ import { RigidBody } from '@react-three/rapier';
3
+ import { Text } from '@react-three/drei';
4
+
5
+ interface MachineNode3DProps {
6
+ id: string;
7
+ position: [number, number, number];
8
+ state: string;
9
+ metadata?: any;
10
+ }
11
+
12
+ export const MachineNode3D: React.FC<MachineNode3DProps> = ({ id, position, state, metadata }) => {
13
+ const isProcessing = state === 'processing';
14
+
15
+ // Color based on state
16
+ const color = isProcessing ? '#fb923c' : '#525252'; // orange-400 : neutral-600
17
+ const emissive = isProcessing ? '#ea580c' : '#000000';
18
+
19
+ return (
20
+ <RigidBody type="fixed" position={position} colliders="cuboid">
21
+ {/* Base platform */}
22
+ <mesh position={[0, -0.4, 0]} receiveShadow>
23
+ <boxGeometry args={[3, 0.2, 3]} />
24
+ <meshStandardMaterial color="#262626" />
25
+ </mesh>
26
+
27
+ {/* Core Machine Body */}
28
+ <mesh castShadow receiveShadow>
29
+ <boxGeometry args={[2, 1, 2]} />
30
+ <meshStandardMaterial
31
+ color={color}
32
+ emissive={emissive}
33
+ emissiveIntensity={isProcessing ? 0.5 : 0}
34
+ />
35
+ </mesh>
36
+
37
+ {/* Machine Name/Label */}
38
+ <Text
39
+ position={[0, 1.2, 0]}
40
+ fontSize={0.3}
41
+ color="white"
42
+ anchorX="center"
43
+ anchorY="middle"
44
+ outlineWidth={0.02}
45
+ outlineColor="#000000"
46
+ >
47
+ {metadata?.name || id}
48
+ </Text>
49
+
50
+ <Text
51
+ position={[0, 0.8, 1.01]}
52
+ fontSize={0.15}
53
+ color={isProcessing ? "#ffed4a" : "#a3a3a3"}
54
+ anchorX="center"
55
+ anchorY="middle"
56
+ >
57
+ {state.toUpperCase()}
58
+ </Text>
59
+ </RigidBody>
60
+ );
61
+ };
@@ -0,0 +1,54 @@
1
+ import React, { useRef } from 'react';
2
+ import { RigidBody, RapierRigidBody } from '@react-three/rapier';
3
+ import { Text } from '@react-three/drei';
4
+
5
+ interface OrderProduct3DProps {
6
+ id: string;
7
+ position: [number, number, number];
8
+ metadata?: any;
9
+ }
10
+
11
+ export const OrderProduct3D: React.FC<OrderProduct3DProps> = ({ id, position }) => {
12
+ const rigidBodyRef = useRef<RapierRigidBody>(null);
13
+
14
+ // Permitimos levantar e impulsar el cubo haciendo click
15
+ const handlePointerDown = (e: any) => {
16
+ e.stopPropagation();
17
+ if (rigidBodyRef.current) {
18
+ // Apply a small upward and forward impulse
19
+ rigidBodyRef.current.applyImpulse({ x: 0, y: 5, z: -2 }, true);
20
+ rigidBodyRef.current.applyTorqueImpulse({ x: Math.random(), y: Math.random(), z: Math.random() }, true);
21
+ }
22
+ };
23
+
24
+ return (
25
+ <RigidBody
26
+ ref={rigidBodyRef}
27
+ type="dynamic"
28
+ position={position}
29
+ colliders="cuboid"
30
+ restitution={0.6} // Bounciness
31
+ friction={0.5}
32
+ >
33
+ <mesh castShadow receiveShadow onPointerDown={handlePointerDown}>
34
+ <boxGeometry args={[0.8, 0.8, 0.8]} />
35
+ <meshStandardMaterial
36
+ color="#facc15" // yellow-400 (wooden box style)
37
+ roughness={0.8}
38
+ />
39
+ </mesh>
40
+
41
+ <Text
42
+ position={[0, 0.5, 0]}
43
+ fontSize={0.2}
44
+ color="black"
45
+ anchorX="center"
46
+ anchorY="middle"
47
+ outlineWidth={0.01}
48
+ outlineColor="white"
49
+ >
50
+ {id}
51
+ </Text>
52
+ </RigidBody>
53
+ );
54
+ };