From 33c70e776a82213794b28918dd1ebe88bc74beee Mon Sep 17 00:00:00 2001 From: echo Date: Fri, 22 May 2026 00:45:17 +0200 Subject: [PATCH] np --- odin/gui_project.comic.json | 214 +++----------------------------- odin/src/gui/actions.odin | 49 ++++++++ odin/src/gui/runtime.odin | 71 ++++++++++- odin/src/gui/summary_views.odin | 134 ++++++++++++++++++++ 4 files changed, 265 insertions(+), 203 deletions(-) diff --git a/odin/gui_project.comic.json b/odin/gui_project.comic.json index f706e82..58579d2 100644 --- a/odin/gui_project.comic.json +++ b/odin/gui_project.comic.json @@ -9,12 +9,12 @@ "last_modified_iso": "" }, "user_mode": 0, - "story_idea": "two balls rolling under the sun", + "story_idea": "3 trees on the wind", "story_genre": "action", "target_audience": "general", "art_style": "manga", "script": { - "title": "Rolling Duel", + "title": "The Last Stand", "synopsis": "Generated comic synopsis", "characters": [ @@ -28,7 +28,7 @@ "panel_id": "panel_001_001", "panel_number": 1, "shot_type": 2, - "description": "A blazing sun dominates the sky, casting harsh light on a vast, empty desert. Two small dots in the distance kick up dust.", + "description": "Wide shot: Three ancient trees stand on a barren hilltop, their branches intertwined. Storm clouds swirl overhead, lightning in the distance. Wind howls, leaves flying.", "characters_present": [ ], @@ -45,7 +45,7 @@ "panel_id": "panel_001_002", "panel_number": 2, "shot_type": 2, - "description": "Close-up on two balls: one red with a fiery pattern, one blue with a water-like swirl. They are rolling fast, side by side. Cracks form in the ground beneath them.", + "description": "Close-up on the middle tree's trunk. Bark cracks open, revealing a glowing, pulsing core of light. The other two trees lean inward, as if protecting it.", "characters_present": [ ], @@ -62,17 +62,12 @@ "panel_id": "panel_001_003", "panel_number": 3, "shot_type": 2, - "description": "The red ball veers sharply left, kicking up a spray of sand. The blue ball mirrors the move, sparks flying from its surface.", + "description": "From the left, a massive tornado approaches, dark and funnel-shaped. Debris swirls around it. The trees brace, roots gripping the ground.", "characters_present": [ ], "dialogue": [ - { - "speaker_id": "", - "text": "VROOM!", - "bubble_type": 0, - "emotion": "" - } + ], "caption": "", "sound_effects": [ @@ -84,29 +79,7 @@ "panel_id": "panel_001_004", "panel_number": 4, "shot_type": 2, - "description": "Red ball takes a ramp-like dune and launches into the air, spinning. Blue ball follows, but slightly lower.", - "characters_present": [ - - ], - "dialogue": [ - { - "speaker_id": "", - "text": "WHOOSH!", - "bubble_type": 0, - "emotion": "" - } - ], - "caption": "", - "sound_effects": [ - - ], - "transition_from_previous": 0 - }, - { - "panel_id": "panel_001_005", - "panel_number": 5, - "shot_type": 2, - "description": "Aerial view: both balls are airborne, shadows on the sand below. Red ball is slightly ahead.", + "description": "The tornado hits the left tree. Its branches snap violently, but it holds firm, roots glowing with energy. Sparks fly where wind meets bark.", "characters_present": [ ], @@ -116,28 +89,6 @@ "caption": "", "sound_effects": [ - ], - "transition_from_previous": 0 - }, - { - "panel_id": "panel_001_006", - "panel_number": 6, - "shot_type": 2, - "description": "They land simultaneously, creating twin craters. Dust clouds obscure them. The sun glints off their surfaces.", - "characters_present": [ - - ], - "dialogue": [ - { - "speaker_id": "", - "text": "BOOM!", - "bubble_type": 0, - "emotion": "" - } - ], - "caption": "", - "sound_effects": [ - ], "transition_from_previous": 0 } @@ -151,7 +102,7 @@ "panel_id": "panel_002_001", "panel_number": 1, "shot_type": 2, - "description": "From the dust, the red ball emerges first, rolling faster. The blue ball is close behind, leaving a trail of steam.", + "description": "The middle tree pulses brighter, sending a shockwave that pushes the tornado back. The right tree extends a branch to shield the core. Wind howls.", "characters_present": [ ], @@ -168,17 +119,12 @@ "panel_id": "panel_002_002", "panel_number": 2, "shot_type": 2, - "description": "Close-up on the red ball: its surface is glowing hot, with tiny flames licking the edges.", + "description": "The tornado splits into two smaller funnels, attacking from both sides. The left tree's roots snap, it starts to topple. The middle tree's core flickers.", "characters_present": [ ], "dialogue": [ - { - "speaker_id": "", - "text": "HISS", - "bubble_type": 0, - "emotion": "" - } + ], "caption": "", "sound_effects": [ @@ -190,17 +136,12 @@ "panel_id": "panel_002_003", "panel_number": 3, "shot_type": 2, - "description": "The blue ball rams into the red ball from the side. They lock, spinning together in a whirlwind of sand.", + "description": "The right tree bends forward, its trunk wrapping around the middle tree, absorbing the impact. The left tree falls, but its roots still glow, transferring energy.", "characters_present": [ ], "dialogue": [ - { - "speaker_id": "", - "text": "CLANG!", - "bubble_type": 0, - "emotion": "" - } + ], "caption": "", "sound_effects": [ @@ -212,29 +153,7 @@ "panel_id": "panel_002_004", "panel_number": 4, "shot_type": 2, - "description": "They separate, skidding to a halt. Both balls are facing each other, a few meters apart. The sun is directly overhead.", - "characters_present": [ - - ], - "dialogue": [ - { - "speaker_id": "", - "text": "SCREECH", - "bubble_type": 0, - "emotion": "" - } - ], - "caption": "", - "sound_effects": [ - - ], - "transition_from_previous": 0 - }, - { - "panel_id": "panel_002_005", - "panel_number": 5, - "shot_type": 2, - "description": "Silence. A single bead of sweat (or condensation) drips from the blue ball. The red ball's glow intensifies.", + "description": "Final wide shot: The storm passes, clouds break. The two remaining trees stand tall, the middle core glowing steady. A single leaf drifts down, landing on the ground. Peace.", "characters_present": [ ], @@ -244,28 +163,6 @@ "caption": "", "sound_effects": [ - ], - "transition_from_previous": 0 - }, - { - "panel_id": "panel_002_006", - "panel_number": 6, - "shot_type": 2, - "description": "Both balls lunge forward at the same time. The panel is a blur of motion lines and dust. The final word:", - "characters_present": [ - - ], - "dialogue": [ - { - "speaker_id": "", - "text": "CRASH!!!", - "bubble_type": 0, - "emotion": "" - } - ], - "caption": "", - "sound_effects": [ - ], "transition_from_previous": 0 } @@ -277,90 +174,7 @@ ], "panel_images": { - "panel_001_001": { - "url": "file:///tmp/comic-gui-local-panels-1597088181/panel_001_panel_001_001.png", - "width": 1024, - "height": 1024, - "seed": 1, - "prompt": "local" - }, - "panel_002_006": { - "url": "file:///tmp/comic-gui-local-panels-1597088181/panel_012_panel_002_006.png", - "width": 1024, - "height": 1024, - "seed": 12, - "prompt": "local" - }, - "panel_001_006": { - "url": "file:///tmp/comic-gui-local-panels-1597088181/panel_006_panel_001_006.png", - "width": 1024, - "height": 1024, - "seed": 6, - "prompt": "local" - }, - "panel_002_001": { - "url": "file:///tmp/comic-gui-local-panels-1597088181/panel_007_panel_002_001.png", - "width": 1024, - "height": 1024, - "seed": 7, - "prompt": "local" - }, - "panel_001_002": { - "url": "file:///tmp/comic-gui-local-panels-1597088181/panel_002_panel_001_002.png", - "width": 1024, - "height": 1024, - "seed": 2, - "prompt": "local" - }, - "panel_002_005": { - "url": "file:///tmp/comic-gui-local-panels-1597088181/panel_011_panel_002_005.png", - "width": 1024, - "height": 1024, - "seed": 11, - "prompt": "local" - }, - "panel_001_004": { - "url": "file:///tmp/comic-gui-local-panels-1597088181/panel_004_panel_001_004.png", - "width": 1024, - "height": 1024, - "seed": 4, - "prompt": "local" - }, - "panel_002_003": { - "url": "file:///tmp/comic-gui-local-panels-1597088181/panel_009_panel_002_003.png", - "width": 1024, - "height": 1024, - "seed": 9, - "prompt": "local" - }, - "panel_001_003": { - "url": "file:///tmp/comic-gui-local-panels-1597088181/panel_003_panel_001_003.png", - "width": 1024, - "height": 1024, - "seed": 3, - "prompt": "local" - }, - "panel_002_004": { - "url": "file:///tmp/comic-gui-local-panels-1597088181/panel_010_panel_002_004.png", - "width": 1024, - "height": 1024, - "seed": 10, - "prompt": "local" - }, - "panel_002_002": { - "url": "file:///tmp/comic-gui-local-panels-1597088181/panel_008_panel_002_002.png", - "width": 1024, - "height": 1024, - "seed": 8, - "prompt": "local" - }, - "panel_001_005": { - "url": "file:///tmp/comic-gui-local-panels-1597088181/panel_005_panel_001_005.png", - "width": 1024, - "height": 1024, - "seed": 5, - "prompt": "local" - } + }, "panel_errors": { diff --git a/odin/src/gui/actions.odin b/odin/src/gui/actions.odin index c2fe18f..4d44988 100644 --- a/odin/src/gui/actions.odin +++ b/odin/src/gui/actions.odin @@ -130,6 +130,55 @@ action_layout_auto :: proc(controller: ^ui.App_Controller) -> string { return "Auto layout generated" } +action_regenerate_page_layout :: proc(controller: ^ui.App_Controller, page_num: int) -> string { + if page_num < 0 || page_num >= len(controller.state.page_layouts) { + return "Invalid layout page" + } + + layout := &controller.state.page_layouts[page_num] + panel_count := len(layout.panels) + if panel_count == 0 { + return "Page has no panels" + } + + // For a simple visible regenerate effect, pick a random pattern that fits + pattern_ids := []string{ + "single-splash", "grid-2x2", "grid-3x3", "dynamic-action", + "manga-spread", "dialogue-heavy", "cinematic-widescreen", + } + + // Just pick the next valid pattern in the list (or wrap around) + start_idx := 0 + for id, i in pattern_ids { + if id == layout.pattern_id { + start_idx = i + break + } + } + + new_pattern_id := layout.pattern_id + for i in 1..=len(pattern_ids) { + idx := (start_idx + i) % len(pattern_ids) + candidate := pattern_ids[idx] + if core.pattern_max_panels(candidate) >= panel_count { + new_pattern_id = candidate + break + } + } + + pattern := core.get_layout_pattern_by_id(new_pattern_id) + layout.pattern_id = new_pattern_id + for i in 0..= len(pattern.cells) { + cell_index = len(pattern.cells) - 1 + } + layout.panels[i].layout_cell = pattern.cells[cell_index] + } + + return "Layout page regenerated" +} + export_format_name :: proc(f: core.Export_Format) -> string { switch f { case .PDF: return "PDF" diff --git a/odin/src/gui/runtime.odin b/odin/src/gui/runtime.odin index b0b4fd8..9ea53c1 100644 --- a/odin/src/gui/runtime.odin +++ b/odin/src/gui/runtime.odin @@ -331,6 +331,26 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error { push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.panel_cursor+1, panel_count)) } } + if controller.active_screen == .Layout && button_clicked(summary_prev_btn) { + layout_page_count := len(controller.state.page_layouts) + if layout_page_count > 0 { + summary_opts.layout_page_cursor -= 1 + if summary_opts.layout_page_cursor < 0 { + summary_opts.layout_page_cursor = layout_page_count - 1 + } + push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count)) + } + } + if controller.active_screen == .Layout && button_clicked(summary_next_btn) { + layout_page_count := len(controller.state.page_layouts) + if layout_page_count > 0 { + summary_opts.layout_page_cursor += 1 + if summary_opts.layout_page_cursor >= layout_page_count { + summary_opts.layout_page_cursor = 0 + } + push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count)) + } + } if button_clicked(new_btn) { if is_dirty && !shift_down { @@ -519,6 +539,26 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error { push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.panel_cursor+1, panel_count)) } } + if controller.active_screen == .Layout && ctrl_down && rl.IsKeyPressed(.LEFT_BRACKET) { + layout_page_count := len(controller.state.page_layouts) + if layout_page_count > 0 { + summary_opts.layout_page_cursor -= 1 + if summary_opts.layout_page_cursor < 0 { + summary_opts.layout_page_cursor = layout_page_count - 1 + } + push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count)) + } + } + if controller.active_screen == .Layout && ctrl_down && rl.IsKeyPressed(.RIGHT_BRACKET) { + layout_page_count := len(controller.state.page_layouts) + if layout_page_count > 0 { + summary_opts.layout_page_cursor += 1 + if summary_opts.layout_page_cursor >= layout_page_count { + summary_opts.layout_page_cursor = 0 + } + push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count)) + } + } if ctrl_down && rl.IsKeyPressed(.MINUS) { push_dirty_status(&is_dirty, &status_msg, &action_log, set_autosave_interval_text(&autosave_interval_text, autosave_secs-5)) } @@ -889,12 +929,12 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error { } else if controller.active_screen == .Panels { draw_small_button(summary_prev_btn, "< Pn") draw_small_button(summary_next_btn, "Pn >") + } else if controller.active_screen == .Layout { + draw_small_button(summary_prev_btn, "< Ly") + draw_small_button(summary_next_btn, "Ly >") } if !compact_mode { hint_label := "Ctrl+[ / Ctrl+]" - if controller.active_screen == .Script || controller.active_screen == .Layout { - hint_label = "Ctrl+H / Ctrl+J" - } draw_hint_pill(rl.Rectangle{x = f32(282 + status_w_loop - 186), y = f32(lower_y_loop + 46), width = 172, height = 20}, hint_label, false) } } @@ -935,6 +975,31 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error { } draw_text_fitted("Ctrl+[ / Ctrl+] panel nav", log_x_loop+18, lower_y_loop+8, 13, int(main_w_loop-status_w_loop-34), 7, TEXT_TERTIARY) + } else if controller.active_screen == .Layout { + regen, new_layout_cursor := draw_layout_detail_panel(controller, log_x_loop, lower_y_loop, main_w_loop-status_w_loop-2, 200, summary_opts.layout_page_cursor) + layout_page_count := len(controller.state.page_layouts) + if summary_opts.layout_page_cursor != new_layout_cursor { + summary_opts.layout_page_cursor = new_layout_cursor + push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count)) + } + if regen { + msg := action_regenerate_page_layout(&controller, summary_opts.layout_page_cursor) + push_status(&status_msg, &action_log, msg) + } + + wheel := rl.GetMouseWheelMove() + if wheel != 0 && layout_page_count > 0 { + summary_opts.layout_page_cursor -= int(wheel) + if summary_opts.layout_page_cursor < 0 { + summary_opts.layout_page_cursor = 0 + } + if summary_opts.layout_page_cursor >= layout_page_count { + summary_opts.layout_page_cursor = layout_page_count - 1 + } + push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count)) + } + + draw_text_fitted("Ctrl+H / Ctrl+J layout nav", log_x_loop+18, lower_y_loop+8, 13, int(main_w_loop-status_w_loop-34), 7, TEXT_TERTIARY) } else { draw_card(rl.Rectangle{x = f32(log_x_loop), y = f32(lower_y_loop), width = f32(main_w_loop-status_w_loop-2), height = 200}) draw_section_title(log_x_loop+18, lower_y_loop+6, "Action Log") diff --git a/odin/src/gui/summary_views.odin b/odin/src/gui/summary_views.odin index 4e959e3..055e210 100644 --- a/odin/src/gui/summary_views.odin +++ b/odin/src/gui/summary_views.odin @@ -441,3 +441,137 @@ draw_panels_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32, } return } + +clamp_layout_cursor :: proc(layout_count, cursor: int) -> int { + if layout_count <= 0 { + return 0 + } + if cursor < 0 { + return 0 + } + if cursor >= layout_count { + return layout_count - 1 + } + return cursor +} + +draw_layout_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32, cursor: int) -> (regen_clicked: bool, new_cursor: int) { + new_cursor = cursor + regen_clicked = false + + draw_card(rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = f32(h)}) + draw_section_title(x+18, y+6, "Layout Detail") + draw_subtle_strip(rl.Rectangle{x = f32(x+12), y = f32(y), width = f32(w-24), height = 34}) + layout_count := len(controller.state.page_layouts) + if layout_count == 0 { + draw_summary_line(x+18, y+46, "No layouts yet. Use Layout Auto after panels.", SUMMARY_HINT) + return + } + idx := clamp_layout_cursor(layout_count, cursor) + layout := controller.state.page_layouts[idx] + + // Header line with status + draw_summary_line(x+18, y+46, fmt.tprintf("Page %d/%d • pattern: %s • %d panels", idx+1, layout_count, layout.pattern_id, len(layout.panels)), SUMMARY_ACCENT) + + // Regenerate button + btn_rec := rl.Rectangle{x = f32(x + w - 100), y = f32(y+42), width = 80, height = 24} + draw_small_button_state(btn_rec, "Regen", true) + if button_clicked(btn_rec) { + regen_clicked = true + } + + // Layout dimensions + draw_summary_subline(x+18, y+68, fmt.tprintf("size: %d x %d", layout.width, layout.height), SUMMARY_SUBLINE) + + // ── Mini wireframe preview ───────────────────────────────── + preview_x := x + 18 + preview_y := y + 88 + preview_max_w: f32 = f32(w) * 0.4 + preview_max_h: f32 = f32(h - 100) + if preview_max_h < 40 { + preview_max_h = 40 + } + + // Scale to fit + lw := f32(layout.width) + lh := f32(layout.height) + if lw < 1 { lw = 1 } + if lh < 1 { lh = 1 } + scale_x := preview_max_w / lw + scale_y := preview_max_h / lh + scale := scale_x + if scale_y < scale { + scale = scale_y + } + pw := lw * scale + ph := lh * scale + + // Draw page outline + page_rec := rl.Rectangle{x = f32(preview_x), y = f32(preview_y), width = pw, height = ph} + rl.DrawRectangleRounded(page_rec, 0.02, 4, BG_STRIP) + rl.DrawRectangleRoundedLinesEx(page_rec, 0.02, 4, 1.0, BORDER_CARD) + + // Draw each panel cell + for i in 0.. 30 && ch > 14 { + draw_text_fitted(num_label, i32(cx + inset + 3), i32(cy + inset + 2), label_sz, int(cw - inset*2 - 4), 5, TEXT_SECONDARY) + } + } + + // ── Page list (right side) ───────────────────────────────── + list_x := x + i32(preview_max_w) + 36 + list_y := y + 88 + list_w := w - i32(preview_max_w) - 54 + row_h: i32 = 18 + rows: i32 = (h - 100) / row_h + if rows < 1 { + rows = 1 + } + start := idx - int(rows/2) + if start < 0 { + start = 0 + } + end := start + int(rows) + if end > layout_count { + end = layout_count + start = end - int(rows) + if start < 0 { + start = 0 + } + } + line: i32 = 0 + for i in start..