Skip to content

Commit b5af5cf

Browse files
committed
feat(frontend): Ronin Pro blueprint simple-stitch modes and multi input slots
- Stitch mode (vertical/horizontal/blend) and dynamic in1..inN ports; workflow graph v7 - Canvas multi connectors; parseWorkflowGraph keeps numbered ports; mixed-edge validation - Home: Key S opens Sprite Sheet adjust; default sprite padding 0; misc UI tweaks Made-with: Cursor
1 parent 71910fd commit b5af5cf

9 files changed

Lines changed: 343 additions & 84 deletions

frontend/src/App.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ function App() {
7777
const { lang, setLang, t } = useLanguage()
7878
const [gemToken, setGemToken] = useState(() => getGemToken())
7979
const [mode, setMode] = useState<AppMode>(null)
80-
/** 首页快捷键进入 RoninPro 子模块(如 R → 自定义缩放) */
80+
/** 首页快捷键:R/T RoninPro 子模块、S → Sprite Sheet 调整等 */
8181
const [roninProDeepLink, setRoninProDeepLink] = useState<string | null>(null)
8282
const consumeRoninProDeepLink = useCallback(() => setRoninProDeepLink(null), [])
8383
const [imageSubMode, setImageSubMode] = useState<ImageSubMode | 'select'>('select')
@@ -96,7 +96,7 @@ function App() {
9696
max_frames: 300,
9797
target_size: { w: 256, h: 256 },
9898
transparent: true,
99-
padding: 4,
99+
padding: 0,
100100
spacing: 0,
101101
layout_mode: 'fixed_columns',
102102
columns: 4,
@@ -218,6 +218,23 @@ function App() {
218218
return () => window.removeEventListener('keydown', onKeyDown)
219219
}, [mode])
220220

221+
/** 首页按 S 进入 Sprite Sheet 调整 */
222+
useEffect(() => {
223+
const onKeyDown = (e: KeyboardEvent) => {
224+
if (mode !== null) return
225+
if (e.code !== 'KeyS') return
226+
if (e.ctrlKey || e.metaKey || e.altKey) return
227+
const el = document.activeElement
228+
const tag = el?.tagName?.toLowerCase()
229+
if (tag === 'input' || tag === 'textarea') return
230+
if (el instanceof HTMLElement && el.isContentEditable) return
231+
e.preventDefault()
232+
setMode('spriteadjust')
233+
}
234+
window.addEventListener('keydown', onKeyDown)
235+
return () => window.removeEventListener('keydown', onKeyDown)
236+
}, [mode])
237+
221238
const wastelandTheme: ThemeConfig = {
222239
token: {
223240
colorPrimary: '#b55233',

frontend/src/components/ImagePixelate.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,8 @@ export default function ImagePixelate() {
269269
const [originalUrl, setOriginalUrl] = useState<string | null>(null)
270270
const [pixelSize, setPixelSize] = useState(8)
271271
const [mergeNearbyStrength, setMergeNearbyStrength] = useState(40)
272-
const [advUpscale, setAdvUpscale] = useState(5)
273-
const [advColors, setAdvColors] = useState(32)
272+
const [advUpscale, setAdvUpscale] = useState(4)
273+
const [advColors, setAdvColors] = useState(64)
274274
const [advScaleResult, setAdvScaleResult] = useState(1)
275275
const [advTransparent, setAdvTransparent] = useState(false)
276276
const [advStatusKey, setAdvStatusKey] = useState<string | null>(null)
@@ -517,7 +517,7 @@ export default function ImagePixelate() {
517517
onChange={(v) => setAdvUpscale(v as number)}
518518
style={{ width: 200, marginRight: 16 }}
519519
/>
520-
<InputNumber min={2} max={7} value={advUpscale} onChange={(v) => setAdvUpscale(v ?? 5)} style={{ width: 90 }} />
520+
<InputNumber min={2} max={7} value={advUpscale} onChange={(v) => setAdvUpscale(v ?? 4)} style={{ width: 90 }} />
521521
</Space>
522522
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>{t('pixelateAdvancedUpscaleHint')}</Text>
523523
</div>
@@ -531,7 +531,7 @@ export default function ImagePixelate() {
531531
onChange={setAdvColors}
532532
style={{ width: 200, marginRight: 16 }}
533533
/>
534-
<InputNumber min={4} max={256} value={advColors} onChange={(v) => setAdvColors(v ?? 32)} style={{ width: 90 }} />
534+
<InputNumber min={4} max={256} value={advColors} onChange={(v) => setAdvColors(v ?? 64)} style={{ width: 90 }} />
535535
</Space>
536536
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>{t('pixelateAdvancedColorsHint')}</Text>
537537
</div>

frontend/src/components/ParamsStep.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ export default function ParamsStep({ file, params, onParamsChange }: Props) {
537537
useEffect(() => {
538538
if (mattedFrames.length > 0) {
539539
const cols = Math.min(12, Math.max(4, Math.ceil(Math.sqrt(mattedFrames.length))))
540-
form.setFieldsValue({ columns: cols, padding: 4, spacing: 0 })
540+
form.setFieldsValue({ columns: cols, padding: 0, spacing: 0 })
541541
}
542542
}, [mattedFrames.length, form])
543543

@@ -551,7 +551,7 @@ export default function ParamsStep({ file, params, onParamsChange }: Props) {
551551
if (mattedFrames.length === 0) return
552552
const targetW = form.getFieldValue('target_w') ?? videoSize?.w ?? 256
553553
const targetH = form.getFieldValue('target_h') ?? videoSize?.h ?? 256
554-
const padding = form.getFieldValue('padding') ?? 4
554+
const padding = form.getFieldValue('padding') ?? 0
555555
const spacing = form.getFieldValue('spacing') ?? 0
556556
const columns = form.getFieldValue('columns') ?? 4
557557
const timestamps = selectedFrameIndices.map((idx) => extractedFrames[idx]?.timestamp ?? 0)
@@ -965,7 +965,7 @@ export default function ParamsStep({ file, params, onParamsChange }: Props) {
965965
target_w: params.target_size?.w ?? 256,
966966
target_h: params.target_size?.h ?? 256,
967967
transparent: params.transparent ?? true,
968-
padding: params.padding ?? 4,
968+
padding: params.padding ?? 0,
969969
spacing: params.spacing ?? 0,
970970
layout_mode: params.layout_mode ?? 'fixed_columns',
971971
columns: params.columns ?? 4,
@@ -981,7 +981,7 @@ export default function ParamsStep({ file, params, onParamsChange }: Props) {
981981
max_frames: (v.max_frames as number) ?? 300,
982982
target_size: { w: (v.target_w as number) ?? 256, h: (v.target_h as number) ?? 256 },
983983
transparent: (v.transparent as boolean) ?? true,
984-
padding: (v.padding as number) ?? 4,
984+
padding: (v.padding as number) ?? 0,
985985
spacing: (v.spacing as number) ?? 0,
986986
layout_mode: (v.layout_mode as 'fixed_columns' | 'auto_square') ?? 'fixed_columns',
987987
columns: (v.columns as number) ?? 4,

frontend/src/components/RoninProCustomWorkflow.tsx

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
buildDefaultRearrangeGrid,
2323
computeWorkflowRunStrategy,
2424
createGraphNode,
25+
getStitchInputSlotCount,
2526
parseWorkflowGraph,
2627
resizeRearrangeGrid,
2728
runDagExecution,
@@ -172,6 +173,33 @@ export default function RoninProCustomWorkflow({ onSendToFineProcess }: RoninPro
172173
)
173174
}, [])
174175

176+
const adjustStitchSlotCount = useCallback((id: string, delta: number) => {
177+
let nextCount: number | null = null
178+
setGraphNodes((list) => {
179+
const node = list.find((x) => x.id === id && x.type === 'simpleStitchVertical')
180+
if (!node) return list
181+
const cur = getStitchInputSlotCount(node)
182+
const next = Math.max(2, Math.min(16, cur + delta))
183+
if (next === cur) return list
184+
nextCount = next
185+
return list.map((n) =>
186+
n.id === id && n.type === 'simpleStitchVertical'
187+
? { ...n, params: { ...n.params, stitchSlotCount: next } }
188+
: n
189+
)
190+
})
191+
if (nextCount !== null) {
192+
setGraphEdges((prev) =>
193+
prev.filter((ed) => {
194+
if (ed.target !== id) return true
195+
const m = /^in(\d+)$/.exec(ed.targetPort ?? '')
196+
if (!m) return true
197+
return parseInt(m[1], 10) <= nextCount!
198+
})
199+
)
200+
}
201+
}, [])
202+
175203
const updateCustomRearrangeDimension = useCallback(
176204
(id: string, key: 'splitCols' | 'splitRows' | 'outRows' | 'outCols', value: number) => {
177205
setGraphNodes((list) =>
@@ -850,8 +878,46 @@ export default function RoninProCustomWorkflow({ onSendToFineProcess }: RoninPro
850878
</div>
851879
</Space>
852880
)
853-
case 'simpleStitchVertical':
854-
return <Text style={{ color: '#8b93a5', fontSize: 11 }}>{t('roninProWorkflowSimpleStitchVerticalHint')}</Text>
881+
case 'simpleStitchVertical': {
882+
const slots = getStitchInputSlotCount(n)
883+
return (
884+
<Space direction="vertical" style={{ width: '100%' }} size="small">
885+
<div>
886+
<Text style={{ color: '#9aa3b5', fontSize: 11 }}>{t('roninProWorkflowStitchMode')}</Text>
887+
<Select
888+
size="small"
889+
style={{ ...INPUT_FULL, marginTop: 4 }}
890+
value={Math.max(0, Math.min(2, Math.round(p.stitchMode ?? 0)))}
891+
options={[
892+
{ value: 0, label: t('roninProWorkflowStitchModeVertical') },
893+
{ value: 1, label: t('roninProWorkflowStitchModeHorizontal') },
894+
{ value: 2, label: t('roninProWorkflowStitchModeOverlay') },
895+
]}
896+
onChange={(v) => updateNodeParam(id, 'stitchMode', typeof v === 'number' ? v : 0)}
897+
/>
898+
</div>
899+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
900+
<Text style={{ color: '#9aa3b5', fontSize: 11 }}>{t('roninProWorkflowStitchInputSlots', { n: slots })}</Text>
901+
<Button
902+
type="dashed"
903+
size="small"
904+
icon={<PlusOutlined />}
905+
disabled={slots >= 16}
906+
onClick={() => adjustStitchSlotCount(id, 1)}
907+
/>
908+
<Button
909+
type="default"
910+
size="small"
911+
disabled={slots <= 2}
912+
onClick={() => adjustStitchSlotCount(id, -1)}
913+
>
914+
915+
</Button>
916+
</div>
917+
<Text style={{ color: '#7a8499', fontSize: 10 }}>{t('roninProWorkflowSimpleStitchVerticalHint')}</Text>
918+
</Space>
919+
)
920+
}
855921
case 'gridDeleteRow':
856922
case 'gridDeleteCol':
857923
case 'gridExpandRow':
@@ -1183,7 +1249,7 @@ export default function RoninProCustomWorkflow({ onSendToFineProcess }: RoninPro
11831249
return null
11841250
}
11851251
},
1186-
[t, updateNodeParam, updateCustomRearrangeDimension, updateCustomRearrangeCell, fillRearrangeGridAutoSequence, fileItems]
1252+
[t, updateNodeParam, adjustStitchSlotCount, updateCustomRearrangeDimension, updateCustomRearrangeCell, fillRearrangeGridAutoSequence, fileItems]
11871253
)
11881254

11891255
return (

frontend/src/components/RoninProUnifySize.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useEffect, useState } from 'react'
22
import { Button, InputNumber, message, Space, Typography, Upload } from 'antd'
33
import { DownloadOutlined } from '@ant-design/icons'
4-
import type { UploadFile } from 'antd'
4+
import type { UploadFile, UploadProps } from 'antd'
55
import { useLanguage } from '../i18n/context'
66
import StashableImage from './StashableImage'
77
import StashDropZone from './StashDropZone'
@@ -129,6 +129,16 @@ export default function RoninProUnifySize() {
129129
}
130130
}
131131

132+
/** 用 onChange 的 fileList 顺序同步,避免多文件拖拽时 beforeUpload 逐文件追加导致顺序颠倒 */
133+
const handleUploadChange: UploadProps['onChange'] = ({ fileList }) => {
134+
const next: File[] = []
135+
for (const item of fileList) {
136+
const o = item.originFileObj
137+
if (o instanceof File) next.push(o)
138+
}
139+
setFiles(next)
140+
}
141+
132142
const downloadResult = () => {
133143
if (!resultUrl) return
134144
const a = document.createElement('a')
@@ -146,15 +156,17 @@ export default function RoninProUnifySize() {
146156
<Dragger
147157
accept={IMAGE_ACCEPT.join(',')}
148158
multiple
149-
fileList={files.map((f, i) => ({ uid: `u-${i}`, name: f.name } as UploadFile))}
150-
beforeUpload={(f) => {
151-
setFiles((prev) => [...prev, f])
152-
return false
153-
}}
154-
onRemove={(file) => {
155-
const idx = files.findIndex((_, i) => `u-${i}` === file.uid)
156-
if (idx >= 0) setFiles((prev) => prev.filter((_, i) => i !== idx))
157-
}}
159+
fileList={files.map(
160+
(f, i) =>
161+
({
162+
uid: `${f.name}-${f.size}-${f.lastModified}-${i}`,
163+
name: f.name,
164+
status: 'done',
165+
originFileObj: f as UploadFile['originFileObj'],
166+
}) as UploadFile,
167+
)}
168+
beforeUpload={() => false}
169+
onChange={handleUploadChange}
158170
>
159171
<p className="ant-upload-text">{t('roninProUnifySizeUploadHint')}</p>
160172
</Dragger>

0 commit comments

Comments
 (0)