@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.
- package/.turbo/turbo-build.log +0 -0
- package/dist/assets/index-YxWqJStM.js +75214 -0
- package/dist/assets/style.css +829 -0
- package/dist/index.html +13 -0
- package/dist/manifest.json +16 -0
- package/dist-server/index.cjs +95 -0
- package/dist-server/index.d.cts +25 -0
- package/dist-server/index.d.ts +25 -0
- package/dist-server/index.js +69 -0
- package/index.html +12 -0
- package/index.ts +26 -0
- package/manifest.ts +24 -0
- package/package.json +67 -0
- package/public/manifest.json +16 -0
- package/server/index.d.ts +1 -0
- package/server/index.js +1 -0
- package/server/package.json +4 -0
- package/src/components/DigitalTwin/PhysicalCanvas.tsx +80 -0
- package/src/components/DigitalTwin/nodes/MachineNode3D.tsx +61 -0
- package/src/components/DigitalTwin/nodes/OrderProduct3D.tsx +54 -0
- package/src/components/SwarmCanvas.tsx +133 -0
- package/src/dev-mount.tsx +10 -0
- package/src/index.css +3 -0
- package/src/logic/rules.ts +39 -0
- package/src/logic/workflowGuard.ts +45 -0
- package/src/stores/digitalTwinStore.ts +66 -0
- package/src/stores/uiStore.ts +19 -0
- package/src/types.ts +38 -0
- package/src/utils/sounds.ts +52 -0
- package/src-backend/index.ts +62 -0
- package/src-backend/migrations.ts +33 -0
- package/tsconfig.json +23 -0
- package/tsup.config.ts +8 -0
- package/vite.config.ts +39 -0
package/dist/index.html
ADDED
|
@@ -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";
|
package/server/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('../dist-server/index.cjs');
|
|
@@ -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
|
+
};
|