Skip to content

Commit 4516a35

Browse files
authored
Merge pull request #5 from BitByte08/main
커스텀 추가
2 parents 2ed7741 + ced1c8b commit 4516a35

16 files changed

Lines changed: 1761 additions & 108 deletions

File tree

app/components/BackButton.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use client';
2+
3+
import { useRouter } from 'next/navigation';
4+
5+
const BackButton = () => {
6+
const router = useRouter();
7+
8+
const handleBack = () => {
9+
router.back();
10+
};
11+
12+
return (
13+
<button
14+
onClick={handleBack}
15+
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
16+
title="뒤로가기"
17+
>
18+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
20+
</svg>
21+
<span>뒤로</span>
22+
</button>
23+
);
24+
};
25+
26+
export default BackButton;

app/components/Stage.tsx

Lines changed: 191 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,33 @@ import FigureSettings from '@/app/components/FigureSettings';
99
import { Pivot } from '@/app/types/figure';
1010

1111
export default function Stage() {
12-
const { project, currentFrameIndex, isPlaying, setCurrentFrameIndex } = useStore();
12+
const {
13+
project,
14+
currentFrameIndex,
15+
isPlaying,
16+
setCurrentFrameIndex,
17+
editorMode,
18+
builderFigure,
19+
builderTool,
20+
selectedPivotIds,
21+
addBuilderPivot,
22+
togglePivotSelection,
23+
setBuilderPivotType,
24+
setBuilderRootPivot,
25+
moveBuilderPivot,
26+
connectingPivots,
27+
addConnectingPivot,
28+
clearConnectingPivots,
29+
createLineFromConnecting
30+
} = useStore();
1331
const [interpolatedFrame, setInterpolatedFrame] = useState(project.frames[currentFrameIndex]);
1432
const svgRef = useRef<SVGSVGElement>(null);
1533
const wasmRef = useRef<any>(null);
1634

1735
const { handleMouseDown, handleMouseMove, handleMouseUp, draggingPivotId, pickerState, setPickerState } = useInteraction(svgRef);
36+
// Builder mode drag state (separate from animation drag)
37+
const [builderDraggingPivotId, setBuilderDraggingPivotId] = useState<string | null>(null);
38+
const [builderDragOffset, setBuilderDragOffset] = useState({ x: 0, y: 0 });
1839
const { renderFigure } = useFigureRender();
1940
const { isOverDeleteZone, handleMouseMove: handleDeleteMouseMove, handleMouseUp: handleDeleteMouseUp } = useDragToDelete();
2041

@@ -114,13 +135,146 @@ export default function Stage() {
114135

115136
const frameToShow = isPlaying ? interpolatedFrame : project.frames[currentFrameIndex];
116137

138+
// Builder mode click handler
139+
const handleBuilderClick = (e: React.MouseEvent<SVGSVGElement>) => {
140+
if (editorMode !== 'figure' || !svgRef.current) return;
141+
142+
const rect = svgRef.current.getBoundingClientRect();
143+
const svgX = ((e.clientX - rect.left) / rect.width) * 1280;
144+
const svgY = ((e.clientY - rect.top) / rect.height) * 720;
145+
146+
if (builderTool === 'add-pivot') {
147+
addBuilderPivot(svgX, svgY);
148+
}
149+
};
150+
151+
// Render builder pivots
152+
const renderBuilderPivots = () => {
153+
if (!builderFigure) return null;
154+
155+
const allPivots: Array<{ pivot: Pivot; parent: Pivot | null }> = [];
156+
const collectPivots = (pivot: Pivot, parent: Pivot | null = null) => {
157+
allPivots.push({ pivot, parent });
158+
pivot.children?.forEach((child) => collectPivots(child, pivot));
159+
};
160+
// root_pivot is a container; collect its children as roots
161+
builderFigure.root_pivot.children.forEach((child) => collectPivots(child, builderFigure.root_pivot));
162+
163+
return (
164+
<g>
165+
{/* Render shapes first */}
166+
{builderFigure.shapes.map((shape, idx) => {
167+
if (shape.type === 'line' && shape.pivotIds.length >= 2) {
168+
const findPivot = (id: string): Pivot | undefined => {
169+
let found: Pivot | undefined;
170+
const search = (p: Pivot) => {
171+
if (p.id === id) { found = p; return; }
172+
p.children?.forEach(search);
173+
};
174+
search(builderFigure.root_pivot);
175+
return found;
176+
};
177+
178+
const p1 = findPivot(shape.pivotIds[0]);
179+
const p2 = findPivot(shape.pivotIds[1]);
180+
181+
if (p1 && p2) {
182+
return (
183+
<line
184+
key={`shape-${idx}`}
185+
x1={p1.x}
186+
y1={p1.y}
187+
x2={p2.x}
188+
y2={p2.y}
189+
stroke={shape.color || builderFigure.color || '#000'}
190+
strokeWidth={builderFigure.thickness || 4}
191+
strokeLinecap="round"
192+
/>
193+
);
194+
}
195+
}
196+
return null;
197+
})}
198+
199+
{/* Render pivots last for proper click handling */}
200+
{allPivots.map(({ pivot, parent }) => {
201+
const isRoot = parent?.id === builderFigure.root_pivot.id;
202+
const isSelected = selectedPivotIds.includes(pivot.id);
203+
204+
let fillColor = '#666';
205+
if (isRoot) fillColor = '#3b82f6'; // Blue for root
206+
else if (pivot.type === 'joint') fillColor = '#f97316'; // Orange for joint
207+
else if (pivot.type === 'fixed') fillColor = '#6b7280'; // Gray for fixed
208+
209+
if (isSelected) fillColor = '#8b5cf6'; // Purple for selected
210+
211+
return (
212+
<circle
213+
key={pivot.id}
214+
cx={pivot.x}
215+
cy={pivot.y}
216+
r={isRoot ? 6 : 4}
217+
fill={fillColor}
218+
stroke="white"
219+
strokeWidth="1.5"
220+
style={{ cursor: 'pointer' }}
221+
onMouseDown={(e) => {
222+
e.stopPropagation();
223+
if (builderTool === 'select') {
224+
if (svgRef.current) {
225+
const CTM = svgRef.current.getScreenCTM();
226+
if (CTM) {
227+
const mouseX = (e.clientX - CTM.e) / CTM.a;
228+
const mouseY = (e.clientY - CTM.f) / CTM.d;
229+
setBuilderDragOffset({ x: mouseX - pivot.x, y: mouseY - pivot.y });
230+
setBuilderDraggingPivotId(pivot.id);
231+
}
232+
}
233+
}
234+
}}
235+
onClick={(e) => {
236+
e.stopPropagation();
237+
if (builderTool === 'select') {
238+
togglePivotSelection(pivot.id);
239+
} else if (builderTool === 'connect') {
240+
// Add to connecting sequence
241+
if (!connectingPivots.includes(pivot.id)) {
242+
addConnectingPivot(pivot.id);
243+
// Auto-create line if 2 pivots selected
244+
if (connectingPivots.length === 1) {
245+
createLineFromConnecting();
246+
}
247+
}
248+
} else if (builderTool === 'set-root') {
249+
setBuilderRootPivot(pivot.id);
250+
} else if (builderTool === 'set-joint') {
251+
setBuilderPivotType(pivot.id, 'joint');
252+
} else if (builderTool === 'set-fixed') {
253+
setBuilderPivotType(pivot.id, 'fixed');
254+
}
255+
}}
256+
/>
257+
);
258+
})}
259+
</g>
260+
);
261+
};
262+
117263
return (
118264
<>
119265
<svg
120266
ref={svgRef}
121-
viewBox="0 0 1280 720"
267+
viewBox="0 0 1280 720"
268+
onClick={editorMode === 'figure' ? handleBuilderClick : undefined}
122269
onMouseMove={(e) => {
123-
if (svgRef.current && draggingPivotId) {
270+
if (editorMode === 'figure' && builderDraggingPivotId && svgRef.current) {
271+
const CTM = svgRef.current.getScreenCTM();
272+
if (CTM) {
273+
const mouseX = (e.clientX - CTM.e) / CTM.a;
274+
const mouseY = (e.clientY - CTM.f) / CTM.d;
275+
moveBuilderPivot(builderDraggingPivotId, mouseX - builderDragOffset.x, mouseY - builderDragOffset.y);
276+
}
277+
} else if (svgRef.current && draggingPivotId) {
124278
const rect = svgRef.current.getBoundingClientRect();
125279
const svgX = ((e.clientX - rect.left) / rect.width) * 1280;
126280
const svgY = ((e.clientY - rect.top) / rect.height) * 720;
@@ -131,7 +285,9 @@ export default function Stage() {
131285
}
132286
}}
133287
onMouseUp={(e) => {
134-
if (svgRef.current && draggingPivotId) {
288+
if (editorMode === 'figure' && builderDraggingPivotId) {
289+
setBuilderDraggingPivotId(null);
290+
} else if (svgRef.current && draggingPivotId) {
135291
const rect = svgRef.current.getBoundingClientRect();
136292
const svgX = ((e.clientX - rect.left) / rect.width) * 1280;
137293
const svgY = ((e.clientY - rect.top) / rect.height) * 720;
@@ -153,34 +309,44 @@ export default function Stage() {
153309
}
154310
}}
155311
onMouseLeave={() => {
312+
setBuilderDraggingPivotId(null);
156313
handleMouseUp();
157314
handleDeleteMouseUp(0, 0, null);
158315
}}
159316
className="bg-surface shadow-sm max-h-[calc(100vh-2rem)] mx-auto"
160317
>
161-
{/* Delete Zone - Trash Icon */}
162-
{/* Delete Zone - Trash Icon (Material) */}
163-
<g opacity={isOverDeleteZone ? 1 : 0.5} style={{ transition: 'opacity 0.2s', filter: isOverDeleteZone ? 'drop-shadow(0 0 8px #ef4444)' : 'none', cursor: 'pointer' }}>
164-
<svg x="1220" y="660" width="36" height="36" viewBox="0 0 24 24" fill="#ef4444">
165-
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
166-
</svg>
167-
</g>
168-
169-
{/* Onion Skinning: Render previous frame if exists and not playing */}
170-
{!isPlaying && currentFrameIndex > 0 && project.frames[currentFrameIndex - 1] && (
171-
<g opacity="0.3" style={{ filter: 'grayscale(100%)' }}>
172-
{project.frames[currentFrameIndex - 1].figures.map(figure =>
173-
renderFigure(figure, null, () => {}) // Non-interactive
174-
)}
318+
{editorMode === 'figure' ? (
319+
// Builder Mode Rendering
320+
<>
321+
{renderBuilderPivots()}
322+
</>
323+
) : (
324+
// Animation Mode Rendering
325+
<>
326+
{/* Delete Zone - Trash Icon */}
327+
<g opacity={isOverDeleteZone ? 1 : 0.5} style={{ transition: 'opacity 0.2s', filter: isOverDeleteZone ? 'drop-shadow(0 0 8px #ef4444)' : 'none', cursor: 'pointer' }}>
328+
<svg x="1220" y="660" width="36" height="36" viewBox="0 0 24 24" fill="#ef4444">
329+
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
330+
</svg>
175331
</g>
176-
)}
177332

178-
{/* Current Frame */}
179-
{frameToShow?.figures.map((figure) => (
180-
<g key={figure.id}>
181-
{renderFigure(figure, draggingPivotId, handleMouseDown)}
182-
</g>
183-
))}
333+
{/* Onion Skinning: Render previous frame if exists and not playing */}
334+
{!isPlaying && currentFrameIndex > 0 && project.frames[currentFrameIndex - 1] && (
335+
<g opacity="0.3" style={{ filter: 'grayscale(100%)' }}>
336+
{project.frames[currentFrameIndex - 1].figures.map(figure =>
337+
renderFigure(figure, null, () => {}) // Non-interactive
338+
)}
339+
</g>
340+
)}
341+
342+
{/* Current Frame */}
343+
{frameToShow?.figures.map((figure) => (
344+
<g key={figure.id}>
345+
{renderFigure(figure, draggingPivotId, handleMouseDown)}
346+
</g>
347+
))}
348+
</>
349+
)}
184350
</svg>
185351
{pickerState.isOpen && pickerState.figureId && (
186352
<FigureSettings

app/components/UpdateNotification.tsx

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
1+
'use client';
2+
13
import { useEffect, useState } from 'react';
24

35
const UpdateNotification = () => {
46
const [updateAvailable, setUpdateAvailable] = useState(false);
57
const [updateDownloaded, setUpdateDownloaded] = useState(false);
68

79
useEffect(() => {
8-
if (typeof window !== 'undefined' && window.electron) {
9-
window.electron.ipcRenderer.on('update-available', () => {
10-
setUpdateAvailable(true);
11-
});
12-
window.electron.ipcRenderer.on('update-downloaded', () => {
13-
setUpdateDownloaded(true);
14-
});
10+
if (typeof window !== 'undefined') {
11+
// Check if we're in Electron environment
12+
const isElectron = !!(window as any).electronAPI;
13+
14+
if (isElectron) {
15+
// Listen for update-available event from main process
16+
if ((window as any).electron?.ipcRenderer) {
17+
(window as any).electron.ipcRenderer.on('update-available', () => {
18+
console.log('Update available');
19+
setUpdateAvailable(true);
20+
});
21+
(window as any).electron.ipcRenderer.on('update-downloaded', () => {
22+
console.log('Update downloaded');
23+
setUpdateDownloaded(true);
24+
setUpdateAvailable(false);
25+
});
26+
27+
// Check for updates on startup
28+
(window as any).electron.ipcRenderer.invoke('check-for-updates')
29+
.catch((err: any) => console.log('Update check skipped (dev mode)', err));
30+
}
31+
}
1532
}
1633
}, []);
1734

1835
const handleUpdate = () => {
19-
if (window.electron) {
20-
window.electron.ipcRenderer.invoke('quit-and-install');
36+
if ((window as any).electron?.ipcRenderer) {
37+
(window as any).electron.ipcRenderer.invoke('quit-and-install');
2138
}
2239
};
2340

@@ -54,3 +71,4 @@ const UpdateNotification = () => {
5471
};
5572

5673
export default UpdateNotification;
74+
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
import UpdateNotification from '@/app/components/UpdateNotification';
2+
13
interface MainContainerProps { children?: React.ReactNode }
2-
const BodyContainer: React.FC<MainContainerProps> = ({children}) => <body className="flex flex-col min-h-screen max-h-screen min-w-screen min-w-screen bg-background p-4 gap-4">{children}</body>
4+
const BodyContainer: React.FC<MainContainerProps> = ({children}) => (
5+
<>
6+
<body className="flex flex-col min-h-screen max-h-screen min-w-screen min-w-screen bg-background p-4 gap-4">
7+
{children}
8+
</body>
9+
<UpdateNotification />
10+
</>
11+
)
312
export default BodyContainer;

0 commit comments

Comments
 (0)