From 30b5f94856a084eb02af8b29a47ef4908f6154ec Mon Sep 17 00:00:00 2001 From: arDTDev Date: Sun, 21 Dec 2025 22:45:00 +0900 Subject: [PATCH] Refactor: Move Export/Import to top nav, enable 3D scene controls --- App.tsx | 219 ++++++++++++++++++++++------------------- Dockerfile | 16 +++ components/Sidebar.tsx | 76 ++++++-------- vite.config.ts | 37 ++++--- 4 files changed, 183 insertions(+), 165 deletions(-) create mode 100644 Dockerfile diff --git a/App.tsx b/App.tsx index e6bebb6..8a2c015 100644 --- a/App.tsx +++ b/App.tsx @@ -5,26 +5,26 @@ import { OrbitControls, Grid } from '@react-three/drei'; import { Sidebar } from './components/Sidebar'; import { IOPanel } from './components/IOPanel'; import { LadderEditor } from './components/LadderEditor'; -import { - SimObject, ObjectType, AxisObject, CylinderObject, SwitchObject, LedObject, +import { + SimObject, ObjectType, AxisObject, CylinderObject, SwitchObject, LedObject, IOLogicRule, LogicCondition, LogicAction, ProjectData } from './types'; import { EditableObject } from './components/SceneObjects'; -import { Layout as LayoutIcon, Cpu, Play, Pause, Square } from 'lucide-react'; +import { Layout as LayoutIcon, Cpu, Play, Pause, Square, Download, Upload } from 'lucide-react'; // --- Simulation Manager (PLC Scan Cycle) --- -const SimulationLoop = ({ - isPlaying, - objects, - setObjects, +const SimulationLoop = ({ + isPlaying, + objects, + setObjects, logicRules, manualInputs, manualOutputs, setInputs, setOutputs -}: { - isPlaying: boolean, - objects: SimObject[], +}: { + isPlaying: boolean, + objects: SimObject[], setObjects: React.Dispatch>, logicRules: IOLogicRule[], manualInputs: boolean[], @@ -32,7 +32,7 @@ const SimulationLoop = ({ setInputs: (inputs: boolean[]) => void, setOutputs: (outputs: boolean[]) => void }) => { - + useFrame((state, delta) => { if (!isPlaying) return; @@ -78,39 +78,39 @@ const SimulationLoop = ({ let changed = false; if (obj.type === ObjectType.LED) { - const led = obj as LedObject; - const shouldBeOn = effectiveOutputs[led.outputPort]; - if (led.isOn !== shouldBeOn) { - newObj = { ...led, isOn: shouldBeOn }; - changed = true; - } + const led = obj as LedObject; + const shouldBeOn = effectiveOutputs[led.outputPort]; + if (led.isOn !== shouldBeOn) { + newObj = { ...led, isOn: shouldBeOn }; + changed = true; + } } else if (obj.type === ObjectType.CYLINDER) { - const cyl = obj as CylinderObject; - const shouldExtend = effectiveOutputs[cyl.outputPort]; - const targetPos = shouldExtend ? cyl.stroke : 0; - if (Math.abs(cyl.currentPosition - targetPos) > 0.01) { - const step = cyl.speed * delta * 5; - if (cyl.currentPosition < targetPos) { - newObj = { ...cyl, extended: shouldExtend, currentPosition: Math.min(targetPos, cyl.currentPosition + step) }; - } else { - newObj = { ...cyl, extended: shouldExtend, currentPosition: Math.max(targetPos, cyl.currentPosition - step) }; - } - changed = true; - } + const cyl = obj as CylinderObject; + const shouldExtend = effectiveOutputs[cyl.outputPort]; + const targetPos = shouldExtend ? cyl.stroke : 0; + if (Math.abs(cyl.currentPosition - targetPos) > 0.01) { + const step = cyl.speed * delta * 5; + if (cyl.currentPosition < targetPos) { + newObj = { ...cyl, extended: shouldExtend, currentPosition: Math.min(targetPos, cyl.currentPosition + step) }; + } else { + newObj = { ...cyl, extended: shouldExtend, currentPosition: Math.max(targetPos, cyl.currentPosition - step) }; + } + changed = true; + } } else if (obj.type === ObjectType.AXIS_LINEAR || obj.type === ObjectType.AXIS_ROTARY) { - const axis = obj as AxisObject; - if (axis.currentValue !== axis.targetValue) { - const diff = axis.targetValue - axis.currentValue; - const step = axis.speed * delta * 10; - if (Math.abs(diff) < step) { - newObj = { ...axis, currentValue: axis.targetValue }; - } else { - newObj = { ...axis, currentValue: axis.currentValue + Math.sign(diff) * step }; - } - changed = true; + const axis = obj as AxisObject; + if (axis.currentValue !== axis.targetValue) { + const diff = axis.targetValue - axis.currentValue; + const step = axis.speed * delta * 10; + if (Math.abs(diff) < step) { + newObj = { ...axis, currentValue: axis.targetValue }; + } else { + newObj = { ...axis, currentValue: axis.currentValue + Math.sign(diff) * step }; } + changed = true; + } } if (changed) hasChanges = true; @@ -127,7 +127,7 @@ export default function App() { const [activeView, setActiveView] = useState<'layout' | 'logic'>('layout'); const [objects, setObjects] = useState([]); const [logicRules, setLogicRules] = useState([]); - + const [inputs, setInputs] = useState(new Array(16).fill(false)); const [outputs, setOutputs] = useState(new Array(16).fill(false)); const [manualInputs, setManualInputs] = useState(new Array(16).fill(false)); @@ -178,11 +178,11 @@ export default function App() { setManualInputs(new Array(16).fill(false)); setManualOutputs(new Array(16).fill(false)); setObjects(prev => prev.map(o => { - if(o.type === ObjectType.AXIS_LINEAR || o.type === ObjectType.AXIS_ROTARY) return {...o, currentValue: (o as AxisObject).min, targetValue: (o as AxisObject).min}; - if(o.type === ObjectType.CYLINDER) return {...o, currentPosition: 0, extended: false}; - if(o.type === ObjectType.LED) return {...o, isOn: false}; - if(o.type === ObjectType.SWITCH) return {...o, isOn: false}; - return o; + if (o.type === ObjectType.AXIS_LINEAR || o.type === ObjectType.AXIS_ROTARY) return { ...o, currentValue: (o as AxisObject).min, targetValue: (o as AxisObject).min }; + if (o.type === ObjectType.CYLINDER) return { ...o, currentPosition: 0, extended: false }; + if (o.type === ObjectType.LED) return { ...o, isOn: false }; + if (o.type === ObjectType.SWITCH) return { ...o, isOn: false }; + return o; })); }; @@ -218,19 +218,19 @@ export default function App() { let newObj: SimObject; switch (type) { case ObjectType.AXIS_LINEAR: - newObj = { id, type, name: 'Linear Axis', position, rotation: {x:0,y:0,z:0}, min: 0, max: 100, currentValue: 0, targetValue: 0, speed: 1, isOscillating: false, triggers: [] } as AxisObject; + newObj = { id, type, name: 'Linear Axis', position, rotation: { x: 0, y: 0, z: 0 }, min: 0, max: 100, currentValue: 0, targetValue: 0, speed: 1, isOscillating: false, triggers: [] } as AxisObject; break; case ObjectType.AXIS_ROTARY: - newObj = { id, type, name: 'Rotary Axis', position, rotation: {x:0,y:0,z:0}, min: 0, max: 360, currentValue: 0, targetValue: 0, speed: 1, isOscillating: false, triggers: [] } as AxisObject; + newObj = { id, type, name: 'Rotary Axis', position, rotation: { x: 0, y: 0, z: 0 }, min: 0, max: 360, currentValue: 0, targetValue: 0, speed: 1, isOscillating: false, triggers: [] } as AxisObject; break; case ObjectType.CYLINDER: - newObj = { id, type, name: 'Cylinder', position, rotation: {x:0,y:0,z:0}, stroke: 2, extended: false, currentPosition: 0, speed: 2, outputPort: 0 } as CylinderObject; + newObj = { id, type, name: 'Cylinder', position, rotation: { x: 0, y: 0, z: 0 }, stroke: 2, extended: false, currentPosition: 0, speed: 2, outputPort: 0 } as CylinderObject; break; case ObjectType.SWITCH: - newObj = { id, type, name: 'Switch', position, rotation: {x:0,y:0,z:0}, isOn: false, isMomentary: true, inputPort: 0 } as SwitchObject; + newObj = { id, type, name: 'Switch', position, rotation: { x: 0, y: 0, z: 0 }, isOn: false, isMomentary: true, inputPort: 0 } as SwitchObject; break; case ObjectType.LED: - newObj = { id, type, name: 'LED', position, rotation: {x:0,y:0,z:0}, isOn: false, color: '#00ff00', outputPort: 0 } as LedObject; + newObj = { id, type, name: 'LED', position, rotation: { x: 0, y: 0, z: 0 }, isOn: false, color: '#00ff00', outputPort: 0 } as LedObject; break; default: return; } @@ -241,7 +241,7 @@ export default function App() { return (
- + {/* Top Navigation */}
@@ -249,61 +249,74 @@ export default function App() {
M
MotionSim
- +
- -
+ +
+ +
+ + +
+
{/* Global Controls */}
- {isPlaying && ( -
-
- PLC ACTIVE -
- )} + {isPlaying && ( +
+
+ PLC ACTIVE +
+ )} -
+
-
- - -
+
+ + +
{/* Main Content Area */}
- + {/* Left Side: Permanent IO Panel */} - - + {objects.map(obj => ( - setObjects(prev => prev.map(o => o.id === id ? {...o, ...updates} as SimObject : o))} + onUpdate={(id, updates) => setObjects(prev => prev.map(o => o.id === id ? { ...o, ...updates } as SimObject : o))} onInteract={(id) => setObjects(prev => prev.map(o => (o.id === id && o.type === ObjectType.SWITCH) ? { ...o, isOn: !o.isOn } as SwitchObject : o))} /> ))} -
- setObjects(prev => prev.map(o => o.id === id ? { ...o, ...updates } as SimObject : o))} onSelect={setSelectedId} onUpdatePortName={handleUpdatePortName} - onExport={handleExport} - onImport={handleImport} />
) : ( - setLogicRules(prev => [...prev, r])} onDeleteRule={(id) => setLogicRules(prev => prev.filter(r => r.id !== id))} - onUpdateRule={(id, updates) => setLogicRules(prev => prev.map(r => r.id === id ? {...r, ...updates} : r))} + onUpdateRule={(id, updates) => setLogicRules(prev => prev.map(r => r.id === id ? { ...r, ...updates } : r))} /> )}
diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..50c8d72 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# 1단계: 빌드 (Node.js) +FROM node:20-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# 2단계: 실행 (Nginx) +FROM nginx:stable-alpine +# 빌드된 파일들을 Nginx의 기본 경로로 복사 +COPY --from=build /app/dist /usr/share/nginx/html +# (선택) 커스텀 nginx 설정을 넣고 싶다면 아래 주석 해제 +# COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 4dce78a..bc0a174 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; -import { - Trash2, - Settings, Link as LinkIcon, Move, RotateCw, Power, +import { + Trash2, + Settings, Link as LinkIcon, Move, RotateCw, Power, Download, Upload, Box } from 'lucide-react'; -import { - SimObject, ObjectType, AxisObject, +import { + SimObject, ObjectType, AxisObject, CylinderObject, LedObject, SwitchObject } from '../types'; @@ -21,8 +21,6 @@ interface SidebarProps { onUpdateObject: (id: string, updates: Partial) => void; onSelect: (id: string | null) => void; onUpdatePortName: (type: 'input' | 'output', index: number, name: string) => void; - onExport: () => void; - onImport: (e: React.ChangeEvent) => void; } export const Sidebar: React.FC = ({ @@ -34,9 +32,7 @@ export const Sidebar: React.FC = ({ onAddObject, onDeleteObject, onUpdateObject, - onUpdatePortName, - onExport, - onImport + onUpdatePortName }) => { const [activeTab, setActiveTab] = useState<'properties' | 'system'>('properties'); const selectedObject = objects.find(o => o.id === selectedId); @@ -50,8 +46,8 @@ export const Sidebar: React.FC = ({ className="w-full bg-gray-950 border border-gray-800 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500 transition-colors" value={value} onChange={(e) => { - const val = type === 'number' ? parseFloat(e.target.value) : e.target.value; - onChange(val); + const val = type === 'number' ? parseFloat(e.target.value) : e.target.value; + onChange(val); }} />
@@ -59,18 +55,18 @@ export const Sidebar: React.FC = ({ return (
- + {/* Sub-navigation */}

Layout Config

- - - -
-

Hardware Mapping

- +

-
Inputs (X0 - X15) +
Inputs (X0 - X15)

{inputNames.map((name, i) => (
X{i.toString().padStart(2, '0')} - = ({

-
Outputs (Y0 - Y15) +
Outputs (Y0 - Y15)

{outputNames.map((name, i) => (
Y{i.toString().padStart(2, '0')} - = ({ }; const ToolBtn: React.FC<{ icon: React.ReactNode, label: string, onClick: () => void }> = ({ icon, label, onClick }) => ( -