check point
This commit is contained in:
1 parent
33c70e776a
commit
b0f9acdb47
37 files changed
+4469
-177
No files matched your search
@@ -476,3 +476,288 @@ autosave_tick_with_message :: proc(project_path: ^string, state: core.Comic_Stat
|
||||
}
|
||||
return fmt.aprintf("Autosave failed: %s", err.message)
|
||||
}
|
||||
|
||||
action_add_bubble :: proc(controller: ^ui.App_Controller, panel_id: string) -> string {
|
||||
id := fmt.aprintf("bubble_%s_new", panel_id)
|
||||
bubble := core.Speech_Bubble{
|
||||
id = id,
|
||||
panel_id = panel_id,
|
||||
type = .Normal,
|
||||
text = "New bubble",
|
||||
position = core.Position{x = 0.1, y = 0.1},
|
||||
size = core.Size{width = 120, height = 40},
|
||||
tail_direction = "bottom",
|
||||
tail_target = core.Position{x = 0.5, y = 0.5},
|
||||
style = core.DEFAULT_BUBBLE_STYLE,
|
||||
speaker_id = "",
|
||||
}
|
||||
|
||||
if controller.state.speech_bubbles == nil {
|
||||
controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble)
|
||||
}
|
||||
|
||||
existing: [dynamic]core.Speech_Bubble
|
||||
if slice, ok := controller.state.speech_bubbles[panel_id]; ok {
|
||||
for b in slice {
|
||||
append(&existing, b)
|
||||
}
|
||||
}
|
||||
append(&existing, bubble)
|
||||
controller.state.speech_bubbles[panel_id] = existing[:]
|
||||
|
||||
return "Added bubble"
|
||||
}
|
||||
|
||||
action_delete_bubble :: proc(controller: ^ui.App_Controller, panel_id: string, bubble_idx: int) -> string {
|
||||
if controller.state.speech_bubbles == nil {
|
||||
return "No bubbles to delete"
|
||||
}
|
||||
slice, ok := controller.state.speech_bubbles[panel_id]
|
||||
if !ok || bubble_idx < 0 || bubble_idx >= len(slice) {
|
||||
return "Bubble not found"
|
||||
}
|
||||
|
||||
// Remove by shifting elements
|
||||
new_slice: [dynamic]core.Speech_Bubble
|
||||
for i in 0..<len(slice) {
|
||||
if i != bubble_idx {
|
||||
append(&new_slice, slice[i])
|
||||
}
|
||||
}
|
||||
if len(new_slice) == 0 {
|
||||
delete_key(&controller.state.speech_bubbles, panel_id)
|
||||
} else {
|
||||
controller.state.speech_bubbles[panel_id] = new_slice[:]
|
||||
}
|
||||
|
||||
return "Deleted bubble"
|
||||
}
|
||||
|
||||
action_auto_place_bubbles_for_panel :: proc(controller: ^ui.App_Controller, panel_id: string, layout: core.Page_Layout) -> string {
|
||||
// Find the script panel to get dialogue
|
||||
script_panel: core.Panel
|
||||
found := false
|
||||
for page in controller.state.script.pages {
|
||||
for p in page.panels {
|
||||
if p.panel_id == panel_id {
|
||||
script_panel = p
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found { break }
|
||||
}
|
||||
if !found {
|
||||
return "Script panel not found for auto-place"
|
||||
}
|
||||
|
||||
// Find the layout cell for this panel to get dimensions
|
||||
cell_w: f32 = 1
|
||||
cell_h: f32 = 1
|
||||
for lp in layout.panels {
|
||||
if lp.panel_id == panel_id {
|
||||
cell_w = lp.layout_cell.w
|
||||
cell_h = lp.layout_cell.h
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
panel_w := cell_w * f32(layout.width)
|
||||
panel_h := cell_h * f32(layout.height)
|
||||
|
||||
bubbles := core.auto_place_panel_bubbles(script_panel, panel_w, panel_h)
|
||||
defer delete(bubbles)
|
||||
|
||||
if len(bubbles) == 0 {
|
||||
return "No dialogue to auto-place"
|
||||
}
|
||||
|
||||
if controller.state.speech_bubbles == nil {
|
||||
controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble)
|
||||
}
|
||||
controller.state.speech_bubbles[panel_id] = bubbles
|
||||
|
||||
return fmt.tprintf("Auto-placed %d bubbles", len(bubbles))
|
||||
}
|
||||
|
||||
action_update_bubble :: proc(controller: ^ui.App_Controller, panel_id: string, bubble_idx: int, new_type: core.Bubble_Type, new_text: string) -> string {
|
||||
if controller.state.speech_bubbles == nil {
|
||||
return "No bubbles to update"
|
||||
}
|
||||
slice, ok := controller.state.speech_bubbles[panel_id]
|
||||
if !ok || bubble_idx < 0 || bubble_idx >= len(slice) {
|
||||
return "Bubble not found"
|
||||
}
|
||||
|
||||
// Copy to dynamic slice, update, then reassign
|
||||
new_slice: [dynamic]core.Speech_Bubble
|
||||
for i in 0..<len(slice) {
|
||||
b := slice[i]
|
||||
if i == bubble_idx {
|
||||
if new_text != "" {
|
||||
b.text = new_text
|
||||
}
|
||||
b.type = new_type
|
||||
}
|
||||
append(&new_slice, b)
|
||||
}
|
||||
controller.state.speech_bubbles[panel_id] = new_slice[:]
|
||||
return "Bubble updated"
|
||||
}
|
||||
|
||||
action_generate_character_reference :: proc(controller: ^ui.App_Controller, character_id: string) -> string {
|
||||
cfg := shared.load_config()
|
||||
if len(cfg.fal_api_key) == 0 {
|
||||
return "FAL_API_KEY is missing"
|
||||
}
|
||||
|
||||
// Find the character
|
||||
target_char: core.Character
|
||||
found := false
|
||||
for c in controller.state.characters {
|
||||
if c.id == character_id {
|
||||
target_char = c
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return "Character not found"
|
||||
}
|
||||
|
||||
url, err := adapters.generate_character_reference_stub(cfg, target_char, controller.state.art_style)
|
||||
if !shared.is_ok(err) {
|
||||
return err.message
|
||||
}
|
||||
|
||||
// Update character with reference image
|
||||
new_chars: [dynamic]core.Character
|
||||
for c in controller.state.characters {
|
||||
new_c := c
|
||||
if c.id == character_id {
|
||||
new_c.reference_image_url = url
|
||||
}
|
||||
append(&new_chars, new_c)
|
||||
}
|
||||
delete(controller.state.characters)
|
||||
controller.state.characters = new_chars[:]
|
||||
|
||||
return fmt.tprintf("Generated reference for %s", target_char.name)
|
||||
}
|
||||
|
||||
action_generate_character_sheet :: proc(controller: ^ui.App_Controller, character_id: string) -> string {
|
||||
cfg := shared.load_config()
|
||||
if len(cfg.fal_api_key) == 0 {
|
||||
return "FAL_API_KEY is missing"
|
||||
}
|
||||
|
||||
// Find the character
|
||||
target_char: core.Character
|
||||
found := false
|
||||
for c in controller.state.characters {
|
||||
if c.id == character_id {
|
||||
target_char = c
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return "Character not found"
|
||||
}
|
||||
|
||||
sheet_urls, err := adapters.generate_character_sheet_stub(cfg, target_char, controller.state.art_style)
|
||||
if !shared.is_ok(err) {
|
||||
return err.message
|
||||
}
|
||||
|
||||
// Update character with sheet URLs
|
||||
new_chars: [dynamic]core.Character
|
||||
for c in controller.state.characters {
|
||||
new_c := c
|
||||
if c.id == character_id {
|
||||
// Free old sheet URLs
|
||||
for u in c.character_sheet_urls {
|
||||
delete(u)
|
||||
}
|
||||
delete(c.character_sheet_urls)
|
||||
new_c.character_sheet_urls = sheet_urls
|
||||
}
|
||||
append(&new_chars, new_c)
|
||||
}
|
||||
delete(controller.state.characters)
|
||||
controller.state.characters = new_chars[:]
|
||||
|
||||
return fmt.tprintf("Generated %d-pose sheet for %s", len(sheet_urls), target_char.name)
|
||||
}
|
||||
|
||||
action_update_bubble_text :: proc(controller: ^ui.App_Controller, bubble_id: string, new_text: string) -> string {
|
||||
if controller.state.speech_bubbles == nil {
|
||||
return "No bubbles to update"
|
||||
}
|
||||
|
||||
for panel_id, slice in controller.state.speech_bubbles {
|
||||
for i in 0..<len(slice) {
|
||||
if slice[i].id == bubble_id {
|
||||
new_slice: [dynamic]core.Speech_Bubble
|
||||
for j in 0..<len(slice) {
|
||||
b := slice[j]
|
||||
if j == i {
|
||||
b.text = new_text
|
||||
}
|
||||
append(&new_slice, b)
|
||||
}
|
||||
controller.state.speech_bubbles[panel_id] = new_slice[:]
|
||||
return "Bubble text updated"
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Bubble not found"
|
||||
}
|
||||
|
||||
action_reposition_bubble :: proc(controller: ^ui.App_Controller, bubble_id: string, x, y: f32) -> string {
|
||||
if controller.state.speech_bubbles == nil {
|
||||
return "No bubbles to reposition"
|
||||
}
|
||||
|
||||
for panel_id, slice in controller.state.speech_bubbles {
|
||||
for i in 0..<len(slice) {
|
||||
if slice[i].id == bubble_id {
|
||||
new_slice: [dynamic]core.Speech_Bubble
|
||||
for j in 0..<len(slice) {
|
||||
b := slice[j]
|
||||
if j == i {
|
||||
b.position = core.Position{x = x, y = y}
|
||||
}
|
||||
append(&new_slice, b)
|
||||
}
|
||||
controller.state.speech_bubbles[panel_id] = new_slice[:]
|
||||
return "Bubble repositioned"
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Bubble not found"
|
||||
}
|
||||
|
||||
action_reset_bubble_position :: proc(controller: ^ui.App_Controller, bubble_id: string) -> string {
|
||||
if controller.state.speech_bubbles == nil {
|
||||
return "No bubbles to reset"
|
||||
}
|
||||
|
||||
for panel_id, slice in controller.state.speech_bubbles {
|
||||
for i in 0..<len(slice) {
|
||||
if slice[i].id == bubble_id {
|
||||
new_slice: [dynamic]core.Speech_Bubble
|
||||
for j in 0..<len(slice) {
|
||||
b := slice[j]
|
||||
if j == i {
|
||||
b.position = core.Position{x = 0, y = 0}
|
||||
}
|
||||
append(&new_slice, b)
|
||||
}
|
||||
controller.state.speech_bubbles[panel_id] = new_slice[:]
|
||||
return "Bubble position reset"
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Bubble not found"
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package gui
|
||||
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
import "../core"
|
||||
import "../ui"
|
||||
|
||||
bubble_type_name :: proc(t: core.Bubble_Type) -> string {
|
||||
switch t {
|
||||
case .Normal: return "Normal"
|
||||
case .Thought: return "Thought"
|
||||
case .Shout: return "Shout"
|
||||
case .Whisper: return "Whisper"
|
||||
case .Narration: return "Narration"
|
||||
case .Sound_Effect: return "SFX"
|
||||
}
|
||||
return "Normal"
|
||||
}
|
||||
|
||||
bubble_type_from_name :: proc(name: string) -> core.Bubble_Type {
|
||||
switch name {
|
||||
case "Normal": return .Normal
|
||||
case "Thought": return .Thought
|
||||
case "Shout": return .Shout
|
||||
case "Whisper": return .Whisper
|
||||
case "Narration": return .Narration
|
||||
case "SFX": return .Sound_Effect
|
||||
}
|
||||
return .Normal
|
||||
}
|
||||
|
||||
clamp_bubble_cursor :: proc(count, cursor: int) -> int {
|
||||
if count <= 0 {
|
||||
return 0
|
||||
}
|
||||
if cursor < 0 {
|
||||
return 0
|
||||
}
|
||||
if cursor >= count {
|
||||
return count - 1
|
||||
}
|
||||
return cursor
|
||||
}
|
||||
|
||||
collect_layout_panels_for_page :: proc(layouts: []core.Page_Layout, page_cursor: int) -> []core.Page_Layout_Panel {
|
||||
if len(layouts) == 0 || page_cursor < 0 || page_cursor >= len(layouts) {
|
||||
return nil
|
||||
}
|
||||
return layouts[page_cursor].panels
|
||||
}
|
||||
|
||||
count_bubbles_for_panel :: proc(bubbles: map[string][]core.Speech_Bubble, panel_id: string) -> int {
|
||||
if bubbles == nil {
|
||||
return 0
|
||||
}
|
||||
if slice, ok := bubbles[panel_id]; ok {
|
||||
return len(slice)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
draw_bubbles_detail_panel :: proc(
|
||||
controller: ui.App_Controller,
|
||||
x, y, w, h: i32,
|
||||
page_cursor: int,
|
||||
panel_cursor: int,
|
||||
bubble_cursor: int,
|
||||
) -> (
|
||||
add_clicked: bool,
|
||||
delete_clicked: bool,
|
||||
auto_place_clicked: bool,
|
||||
new_page_cursor: int,
|
||||
new_panel_cursor: int,
|
||||
new_bubble_cursor: int,
|
||||
edited_text: string,
|
||||
edited_type: core.Bubble_Type,
|
||||
type_changed: bool,
|
||||
text_changed: bool,
|
||||
) {
|
||||
add_clicked = false
|
||||
delete_clicked = false
|
||||
auto_place_clicked = false
|
||||
new_page_cursor = page_cursor
|
||||
new_panel_cursor = panel_cursor
|
||||
new_bubble_cursor = bubble_cursor
|
||||
type_changed = false
|
||||
text_changed = false
|
||||
edited_text = ""
|
||||
edited_type = .Normal
|
||||
|
||||
draw_card(rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = f32(h)})
|
||||
draw_section_title(x+18, y+6, "Bubble Editor")
|
||||
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. Run Layout Auto first.", SUMMARY_HINT)
|
||||
return
|
||||
}
|
||||
|
||||
page_idx := clamp_layout_cursor(layout_count, page_cursor)
|
||||
if page_idx != page_cursor {
|
||||
new_page_cursor = page_idx
|
||||
}
|
||||
layout := controller.state.page_layouts[page_idx]
|
||||
panel_list := layout.panels
|
||||
panel_count := len(panel_list)
|
||||
|
||||
if panel_count == 0 {
|
||||
draw_summary_line(x+18, y+46, fmt.tprintf("Page %d has no panels", layout.page_number), SUMMARY_HINT)
|
||||
return
|
||||
}
|
||||
|
||||
// Page navigation
|
||||
draw_summary_line(x+18, y+46, fmt.tprintf("Page %d/%d • %d panels", page_idx+1, layout_count, panel_count), SUMMARY_ACCENT)
|
||||
|
||||
panel_idx := clamp_bubble_cursor(panel_count, panel_cursor)
|
||||
if panel_idx != panel_cursor {
|
||||
new_panel_cursor = panel_idx
|
||||
}
|
||||
panel := panel_list[panel_idx]
|
||||
|
||||
// Panel info + auto-place button
|
||||
draw_summary_subline(x+18, y+66, fmt.tprintf("Panel %d (id: %s)", panel.panel_number, panel.panel_id), SUMMARY_SUBLINE)
|
||||
|
||||
auto_btn_rec := rl.Rectangle{x = f32(x + w - 110), y = f32(y+62), width = 90, height = 22}
|
||||
draw_small_button_state(auto_btn_rec, "Auto Place", true)
|
||||
if button_clicked(auto_btn_rec) {
|
||||
auto_place_clicked = true
|
||||
}
|
||||
|
||||
// Bubble list
|
||||
bubble_map := controller.state.speech_bubbles
|
||||
bubbles_for_panel: []core.Speech_Bubble = nil
|
||||
if bubble_map != nil {
|
||||
if slice, ok := bubble_map[panel.panel_id]; ok {
|
||||
bubbles_for_panel = slice
|
||||
}
|
||||
}
|
||||
bubble_count := len(bubbles_for_panel)
|
||||
|
||||
bubble_idx := clamp_bubble_cursor(bubble_count, bubble_cursor)
|
||||
if bubble_count > 0 && bubble_idx != bubble_cursor {
|
||||
new_bubble_cursor = bubble_idx
|
||||
}
|
||||
|
||||
// Bubble list header
|
||||
list_y := y + 90
|
||||
draw_summary_line(x+18, list_y, fmt.tprintf("Bubbles: %d", bubble_count), SUMMARY_ACCENT)
|
||||
|
||||
// Add button
|
||||
add_btn_rec := rl.Rectangle{x = f32(x + w - 70), y = f32(list_y-4), width = 50, height = 20}
|
||||
draw_small_button_state(add_btn_rec, "Add", true)
|
||||
if button_clicked(add_btn_rec) {
|
||||
add_clicked = true
|
||||
}
|
||||
|
||||
// Bubble rows
|
||||
row_start_y := list_y + 22
|
||||
row_h: i32 = 20
|
||||
max_rows: int = int(h - 120) / int(row_h)
|
||||
if max_rows < 1 {
|
||||
max_rows = 1
|
||||
}
|
||||
|
||||
// Scroll window for bubble list
|
||||
scroll_start := 0
|
||||
if bubble_idx >= max_rows {
|
||||
scroll_start = bubble_idx - max_rows + 1
|
||||
}
|
||||
scroll_end := scroll_start + max_rows
|
||||
if scroll_end > bubble_count {
|
||||
scroll_end = bubble_count
|
||||
scroll_start = scroll_end - max_rows
|
||||
if scroll_start < 0 {
|
||||
scroll_start = 0
|
||||
}
|
||||
}
|
||||
|
||||
row: i32 = 0
|
||||
for i in scroll_start..<scroll_end {
|
||||
b := bubbles_for_panel[i]
|
||||
mark := " "
|
||||
row_color := TEXT_TERTIARY
|
||||
if i == bubble_idx {
|
||||
mark = ">"
|
||||
row_color = TEXT_PRIMARY
|
||||
}
|
||||
|
||||
row_rec := rl.Rectangle{x = f32(x+18), y = f32(row_start_y+row*row_h), width = f32(w-90), height = f32(row_h)}
|
||||
if rl.CheckCollisionPointRec(rl.GetMousePosition(), row_rec) && rl.IsMouseButtonPressed(.LEFT) {
|
||||
new_bubble_cursor = i
|
||||
}
|
||||
if rl.CheckCollisionPointRec(rl.GetMousePosition(), row_rec) && row_color == TEXT_TERTIARY {
|
||||
row_color = TEXT_SECONDARY
|
||||
}
|
||||
|
||||
// Delete button on hover/selected
|
||||
del_rec := rl.Rectangle{x = f32(x+w-28), y = f32(row_start_y+row*row_h), width = 18, height = f32(row_h)}
|
||||
if rl.CheckCollisionPointRec(rl.GetMousePosition(), del_rec) && rl.IsMouseButtonPressed(.LEFT) && i == bubble_idx {
|
||||
delete_clicked = true
|
||||
}
|
||||
if i == bubble_idx {
|
||||
draw_text_fitted("x", x+w-26, row_start_y+row*row_h+3, 12, 14, 7, ERROR)
|
||||
}
|
||||
|
||||
preview := b.text
|
||||
if len(preview) > 30 {
|
||||
preview = preview[:30]
|
||||
preview = fmt.tprintf("%s...", preview)
|
||||
}
|
||||
if len(preview) == 0 {
|
||||
preview = "(empty)"
|
||||
}
|
||||
draw_summary_subline(x+18, row_start_y+row*row_h+2,
|
||||
fit_text_for_width(fmt.tprintf("%s [%s] %s", mark, bubble_type_name(b.type), preview), int(w-100), 7), row_color)
|
||||
row += 1
|
||||
}
|
||||
|
||||
// Selected bubble editor
|
||||
if bubble_count > 0 && bubble_idx < bubble_count {
|
||||
editor_y := row_start_y + row*row_h + 8
|
||||
if editor_y < y+h-80 {
|
||||
selected := bubbles_for_panel[bubble_idx]
|
||||
draw_subtle_strip(rl.Rectangle{x = f32(x+12), y = f32(editor_y-6), width = f32(w-24), height = 70})
|
||||
draw_summary_line(x+18, editor_y, fmt.tprintf("Editing: %s", bubble_type_name(selected.type)), SUMMARY_ACCENT)
|
||||
|
||||
// Type selector buttons
|
||||
type_y := editor_y + 20
|
||||
type_idx: i32 = 0
|
||||
types := []core.Bubble_Type{.Normal, .Thought, .Shout, .Whisper, .Narration, .Sound_Effect}
|
||||
type_w: i32 = 60
|
||||
type_spacing: i32 = 4
|
||||
for t in types {
|
||||
t_rec := rl.Rectangle{x = f32(x)+18+f32(int(type_idx)*int(type_w+type_spacing)), y = f32(type_y), width = f32(type_w), height = 18}
|
||||
draw_nav_item(t_rec, bubble_type_name(t), selected.type == t)
|
||||
if button_clicked(t_rec) && selected.type != t {
|
||||
edited_type = t
|
||||
type_changed = true
|
||||
}
|
||||
type_idx += 1
|
||||
}
|
||||
|
||||
// Text preview
|
||||
text_y := type_y + 22
|
||||
draw_summary_subline(x+18, text_y, fit_text_for_width(fmt.tprintf("text: %s", selected.text), int(w-36), 7), SUMMARY_SUBLINE)
|
||||
}
|
||||
}
|
||||
|
||||
if bubble_count == 0 {
|
||||
draw_summary_line(x+18, row_start_y, "No bubbles for this panel. Click Add or Auto Place.", SUMMARY_HINT)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -41,7 +41,7 @@ build_local_script :: proc(story_idea: string, pages: int) -> core.Comic_Script
|
||||
chars_present := make([]string, 1)
|
||||
chars_present[0] = "char_001"
|
||||
dialogue := make([]core.Dialogue, 1)
|
||||
dialogue[0] = core.Dialogue{speaker_id = "char_001", text = "Let's do this.", bubble_type = .Normal, emotion = "neutral"}
|
||||
dialogue[0] = core.Dialogue{speaker_id = "char_001", text = "Let's do this.", bubble_type = .Normal, emotion = .Neutral}
|
||||
|
||||
panel := core.Panel{
|
||||
panel_id = local_panel_id_by_index(i),
|
||||
@@ -75,13 +75,29 @@ build_local_panel_images :: proc(panels: []core.Panel) -> (map[string]core.Panel
|
||||
name := fmt.aprintf("panel_%03d_%s.png", idx+1, p.panel_id)
|
||||
out_path := fmt.aprintf("%s/%s", tmp_dir, name)
|
||||
delete(name)
|
||||
if werr := os.write_entire_file(out_path, "LOCAL PANEL IMAGE"); werr != nil {
|
||||
|
||||
// Create a real PNG image using python3
|
||||
gen_w := 1024
|
||||
gen_h := 1024
|
||||
py_script := fmt.aprintf(
|
||||
"import struct,zlib,sys;w,h=%d,%d;rows=[]\nfor _ in range(h): rows.append(b'\\x00'+b'\\xff\\xff\\xff'*w)\nraw=b''.join(rows);comp=zlib.compress(raw)\ndef crc(d): return struct.pack('>I',zlib.crc32(d)&0xffffffff)\nf=open(sys.argv[1],'wb')\nf.write(b'\\x89PNG\\r\\n\\x1a\\n')\nihdr_data=struct.pack('>IIBBBBB',w,h,8,2,0,0,0)\nf.write(struct.pack('>I',13)+b'IHDR'+ihdr_data+crc(b'IHDR'+ihdr_data))\nf.write(struct.pack('>I',len(comp))+b'IDAT'+comp+crc(b'IDAT'+comp))\nf.write(struct.pack('>I',0)+b'IEND'+crc(b'IEND'))\nf.close()",
|
||||
gen_w, gen_h,
|
||||
)
|
||||
defer delete(py_script)
|
||||
|
||||
py_cmd := [4]string{"python3", "-c", py_script, out_path}
|
||||
desc := os.Process_Desc{command = py_cmd[:]}
|
||||
state, _, stderr, cerr := os.process_exec(desc, context.temp_allocator)
|
||||
if cerr != nil || !state.exited || state.exit_code != 0 {
|
||||
delete(out_path)
|
||||
return nil, shared.new_error(.Generation, "failed writing local panel image", true)
|
||||
msg := fmt.aprintf("failed to create panel image: %s", string(stderr))
|
||||
defer delete(msg)
|
||||
return nil, shared.new_error(.Generation, msg, true)
|
||||
}
|
||||
|
||||
url := fmt.aprintf("file://%s", out_path)
|
||||
prompt := fmt.aprintf("local")
|
||||
images[p.panel_id] = core.Panel_Image{url = url, width = 1024, height = 1024, seed = i64(idx + 1), prompt = prompt}
|
||||
prompt := fmt.aprintf("local panel %d", idx+1)
|
||||
images[p.panel_id] = core.Panel_Image{url = url, width = gen_w, height = gen_h, seed = i64(idx + 1), prompt = prompt}
|
||||
delete(out_path)
|
||||
}
|
||||
return images, shared.ok()
|
||||
|
||||
+173
-24
@@ -49,19 +49,13 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
for !rl.WindowShouldClose() {
|
||||
screen_w_loop := rl.GetScreenWidth()
|
||||
screen_h_loop := rl.GetScreenHeight()
|
||||
compact_mode := screen_h_loop < 860
|
||||
compact_mode := shared.is_compact(screen_h_loop)
|
||||
cfg_loop := shared.load_config()
|
||||
has_deepseek_key := len(cfg_loop.deepseek_api_key) > 0
|
||||
main_w_loop := screen_w_loop - 282 - 20
|
||||
if main_w_loop < 960 {
|
||||
main_w_loop = 960
|
||||
}
|
||||
main_w_loop := shared.compute_main_width(screen_w_loop)
|
||||
status_w_loop := (main_w_loop - 2) / 2
|
||||
log_x_loop := 282 + status_w_loop + 2
|
||||
lower_y_loop := screen_h_loop - 252
|
||||
if lower_y_loop < 450 {
|
||||
lower_y_loop = 450
|
||||
}
|
||||
log_x_loop := shared.LAYOUT.sidebar_width + status_w_loop + 2
|
||||
lower_y_loop := shared.compute_lower_y(screen_h_loop)
|
||||
|
||||
idea_rec := rl.Rectangle{x = 420, y = 90, width = f32(main_w_loop-460), height = 36}
|
||||
genre_rec := rl.Rectangle{x = 420, y = 146, width = f32(main_w_loop-460), height = 36}
|
||||
@@ -125,8 +119,8 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
path_fix_export_status_btn := rl.Rectangle{x = 674, y = 556, width = 32, height = 20}
|
||||
path_fix_all_status_btn := rl.Rectangle{x = 710, y = 556, width = 34, height = 20}
|
||||
|
||||
summary_show_btn := rl.Rectangle{x = f32(282 + status_w_loop - 178), y = f32(lower_y_loop + 18), width = 78, height = 24}
|
||||
summary_sort_btn := rl.Rectangle{x = f32(282 + status_w_loop - 94), y = f32(lower_y_loop + 18), width = 78, height = 24}
|
||||
summary_show_btn := rl.Rectangle{x = f32(shared.LAYOUT.sidebar_width + status_w_loop - 178), y = f32(lower_y_loop + 18), width = 78, height = 24}
|
||||
summary_sort_btn := rl.Rectangle{x = f32(shared.LAYOUT.sidebar_width + status_w_loop - 94), y = f32(lower_y_loop + 18), width = 78, height = 24}
|
||||
summary_prev_btn := rl.Rectangle{x = f32(300), y = f32(lower_y_loop + 18), width = 52, height = 24}
|
||||
summary_next_btn := rl.Rectangle{x = f32(358), y = f32(lower_y_loop + 18), width = 52, height = 24}
|
||||
|
||||
@@ -351,6 +345,36 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
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 == .Bubbles && button_clicked(summary_prev_btn) {
|
||||
layout_page_count := len(controller.state.page_layouts)
|
||||
if layout_page_count > 0 {
|
||||
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
|
||||
panel_count := len(controller.state.page_layouts[page_idx].panels)
|
||||
if panel_count > 0 {
|
||||
summary_opts.bubble_panel_cursor -= 1
|
||||
if summary_opts.bubble_panel_cursor < 0 {
|
||||
summary_opts.bubble_panel_cursor = panel_count - 1
|
||||
}
|
||||
summary_opts.bubble_edit_cursor = 0
|
||||
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.bubble_panel_cursor+1, panel_count))
|
||||
}
|
||||
}
|
||||
}
|
||||
if controller.active_screen == .Bubbles && button_clicked(summary_next_btn) {
|
||||
layout_page_count := len(controller.state.page_layouts)
|
||||
if layout_page_count > 0 {
|
||||
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
|
||||
panel_count := len(controller.state.page_layouts[page_idx].panels)
|
||||
if panel_count > 0 {
|
||||
summary_opts.bubble_panel_cursor += 1
|
||||
if summary_opts.bubble_panel_cursor >= panel_count {
|
||||
summary_opts.bubble_panel_cursor = 0
|
||||
}
|
||||
summary_opts.bubble_edit_cursor = 0
|
||||
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.bubble_panel_cursor+1, panel_count))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if button_clicked(new_btn) {
|
||||
if is_dirty && !shift_down {
|
||||
@@ -559,6 +583,36 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
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 == .Bubbles && ctrl_down && rl.IsKeyPressed(.LEFT_BRACKET) {
|
||||
layout_page_count := len(controller.state.page_layouts)
|
||||
if layout_page_count > 0 {
|
||||
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
|
||||
panel_count := len(controller.state.page_layouts[page_idx].panels)
|
||||
if panel_count > 0 {
|
||||
summary_opts.bubble_panel_cursor -= 1
|
||||
if summary_opts.bubble_panel_cursor < 0 {
|
||||
summary_opts.bubble_panel_cursor = panel_count - 1
|
||||
}
|
||||
summary_opts.bubble_edit_cursor = 0
|
||||
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.bubble_panel_cursor+1, panel_count))
|
||||
}
|
||||
}
|
||||
}
|
||||
if controller.active_screen == .Bubbles && ctrl_down && rl.IsKeyPressed(.RIGHT_BRACKET) {
|
||||
layout_page_count := len(controller.state.page_layouts)
|
||||
if layout_page_count > 0 {
|
||||
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
|
||||
panel_count := len(controller.state.page_layouts[page_idx].panels)
|
||||
if panel_count > 0 {
|
||||
summary_opts.bubble_panel_cursor += 1
|
||||
if summary_opts.bubble_panel_cursor >= panel_count {
|
||||
summary_opts.bubble_panel_cursor = 0
|
||||
}
|
||||
summary_opts.bubble_edit_cursor = 0
|
||||
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.bubble_panel_cursor+1, panel_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))
|
||||
}
|
||||
@@ -690,10 +744,7 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
rl.ClearBackground(BG_BASE)
|
||||
screen_w := rl.GetScreenWidth()
|
||||
screen_h := rl.GetScreenHeight()
|
||||
main_w := screen_w - 282 - 20
|
||||
if main_w < 960 {
|
||||
main_w = 960
|
||||
}
|
||||
main_w := shared.compute_main_width(screen_w)
|
||||
|
||||
rl.DrawRectangle(0, 0, 260, screen_h, BG_SIDEBAR)
|
||||
rl.DrawRectangle(260, 0, screen_w-260, 72, BG_TOPBAR)
|
||||
@@ -716,7 +767,7 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
draw_sidebar_shortcuts(screen_h_loop)
|
||||
|
||||
// --- Pipeline Stepper in Topbar ---
|
||||
draw_card(rl.Rectangle{x = 282, y = 12, width = f32(main_w), height = 48})
|
||||
draw_card(rl.Rectangle{x = f32(shared.LAYOUT.sidebar_width), y = 12, width = f32(main_w), height = 48})
|
||||
rl.DrawText(fmt.ctprintf("%s", ui.screen_name(controller.active_screen)), 300, 26, 20, BRAND_TITLE)
|
||||
|
||||
script_ok := len(controller.state.script.pages) > 0
|
||||
@@ -768,8 +819,8 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
rl.DrawText("Local script pages", screen_w_loop - 250, 26, 16, TEXT_SECONDARY)
|
||||
draw_input_field(pages_rec, local_script_pages, selected_field == 4)
|
||||
|
||||
draw_card(rl.Rectangle{x = 282, y = 82, width = f32(main_w), height = 356})
|
||||
draw_card(rl.Rectangle{x = 282, y = 460, width = f32(main_w), height = 110})
|
||||
draw_card(rl.Rectangle{x = f32(shared.LAYOUT.sidebar_width), y = 82, width = f32(main_w), height = 356})
|
||||
draw_card(rl.Rectangle{x = f32(shared.LAYOUT.sidebar_width), y = 460, width = f32(main_w), height = 110})
|
||||
draw_section_title(300, 92, "Project Setup")
|
||||
draw_section_title(300, 470, "Actions")
|
||||
|
||||
@@ -867,11 +918,11 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
}
|
||||
}
|
||||
|
||||
draw_card(rl.Rectangle{x = 282, y = f32(lower_y_loop-140), width = f32(status_w_loop), height = 160})
|
||||
draw_card(rl.Rectangle{x = f32(shared.LAYOUT.sidebar_width), y = f32(lower_y_loop-140), width = f32(status_w_loop), height = 160})
|
||||
now_draw := rl.GetTime()
|
||||
status_y := lower_y_loop - 126
|
||||
rl.DrawText("Status", 300, status_y, 19, BRAND_TITLE)
|
||||
rl.DrawLine(370, status_y+10, i32(282+status_w_loop)-14, status_y+10, BORDER_DIVIDER)
|
||||
rl.DrawLine(370, status_y+10, i32(shared.LAYOUT.sidebar_width+status_w_loop)-14, status_y+10, BORDER_DIVIDER)
|
||||
draw_text_fitted(status_msg, 300, status_y+26, 18, int(status_w_loop-36), 8, status_text_color(status_msg))
|
||||
draw_readiness_row(controller, 300, status_y+50)
|
||||
ready_count, total_count := ready_stage_count(controller)
|
||||
@@ -901,7 +952,7 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
draw_small_button_state(path_fix_all_status_btn, "PE", !project_path_ok || !export_path_ok)
|
||||
|
||||
draw_screen_summary(controller, export_path, 300, lower_y_loop+12, status_w_loop-8, summary_opts)
|
||||
if controller.active_screen == .Script || controller.active_screen == .Layout || controller.active_screen == .Panels {
|
||||
if controller.active_screen == .Script || controller.active_screen == .Layout || controller.active_screen == .Panels || controller.active_screen == .Bubbles {
|
||||
if controller.active_screen == .Script || controller.active_screen == .Layout {
|
||||
show_txt := "Top"
|
||||
sort_txt := "Asc"
|
||||
@@ -932,10 +983,13 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
} else if controller.active_screen == .Layout {
|
||||
draw_small_button(summary_prev_btn, "< Ly")
|
||||
draw_small_button(summary_next_btn, "Ly >")
|
||||
} else if controller.active_screen == .Bubbles {
|
||||
draw_small_button(summary_prev_btn, "< Pn")
|
||||
draw_small_button(summary_next_btn, "Pn >")
|
||||
}
|
||||
if !compact_mode {
|
||||
hint_label := "Ctrl+[ / Ctrl+]"
|
||||
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)
|
||||
draw_hint_pill(rl.Rectangle{x = f32(shared.LAYOUT.sidebar_width + status_w_loop - 186), y = f32(lower_y_loop + 46), width = 172, height = 20}, hint_label, false)
|
||||
}
|
||||
}
|
||||
if !compact_mode {
|
||||
@@ -985,6 +1039,7 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
if regen {
|
||||
msg := action_regenerate_page_layout(&controller, summary_opts.layout_page_cursor)
|
||||
push_status(&status_msg, &action_log, msg)
|
||||
is_dirty = true
|
||||
}
|
||||
|
||||
wheel := rl.GetMouseWheelMove()
|
||||
@@ -999,7 +1054,101 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
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)
|
||||
draw_text_fitted("Ctrl+[ / Ctrl+] layout 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 == .Bubbles {
|
||||
add_clicked, delete_clicked, auto_place_clicked, new_page_cursor, new_panel_cursor, new_bubble_cursor, edited_text, edited_type, type_changed, text_changed := draw_bubbles_detail_panel(
|
||||
controller, log_x_loop, lower_y_loop, main_w_loop-status_w_loop-2, 200,
|
||||
summary_opts.bubble_page_cursor, summary_opts.bubble_panel_cursor, summary_opts.bubble_edit_cursor,
|
||||
)
|
||||
if summary_opts.bubble_page_cursor != new_page_cursor {
|
||||
summary_opts.bubble_page_cursor = new_page_cursor
|
||||
summary_opts.bubble_panel_cursor = 0
|
||||
summary_opts.bubble_edit_cursor = 0
|
||||
}
|
||||
if summary_opts.bubble_panel_cursor != new_panel_cursor {
|
||||
summary_opts.bubble_panel_cursor = new_panel_cursor
|
||||
summary_opts.bubble_edit_cursor = 0
|
||||
}
|
||||
if summary_opts.bubble_edit_cursor != new_bubble_cursor {
|
||||
summary_opts.bubble_edit_cursor = new_bubble_cursor
|
||||
}
|
||||
if add_clicked {
|
||||
layout_page_count := len(controller.state.page_layouts)
|
||||
if layout_page_count > 0 {
|
||||
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
|
||||
layout := controller.state.page_layouts[page_idx]
|
||||
if len(layout.panels) > 0 {
|
||||
panel_idx := clamp_bubble_cursor(len(layout.panels), summary_opts.bubble_panel_cursor)
|
||||
panel_id := layout.panels[panel_idx].panel_id
|
||||
msg := action_add_bubble(&controller, panel_id)
|
||||
push_status(&status_msg, &action_log, msg)
|
||||
is_dirty = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if delete_clicked {
|
||||
layout_page_count := len(controller.state.page_layouts)
|
||||
if layout_page_count > 0 {
|
||||
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
|
||||
layout := controller.state.page_layouts[page_idx]
|
||||
if len(layout.panels) > 0 {
|
||||
panel_idx := clamp_bubble_cursor(len(layout.panels), summary_opts.bubble_panel_cursor)
|
||||
panel_id := layout.panels[panel_idx].panel_id
|
||||
msg := action_delete_bubble(&controller, panel_id, summary_opts.bubble_edit_cursor)
|
||||
push_status(&status_msg, &action_log, msg)
|
||||
is_dirty = true
|
||||
if summary_opts.bubble_edit_cursor > 0 {
|
||||
summary_opts.bubble_edit_cursor -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if auto_place_clicked {
|
||||
layout_page_count := len(controller.state.page_layouts)
|
||||
if layout_page_count > 0 {
|
||||
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
|
||||
layout := controller.state.page_layouts[page_idx]
|
||||
if len(layout.panels) > 0 {
|
||||
panel_idx := clamp_bubble_cursor(len(layout.panels), summary_opts.bubble_panel_cursor)
|
||||
panel_id := layout.panels[panel_idx].panel_id
|
||||
msg := action_auto_place_bubbles_for_panel(&controller, panel_id, layout)
|
||||
push_status(&status_msg, &action_log, msg)
|
||||
is_dirty = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if type_changed {
|
||||
layout_page_count := len(controller.state.page_layouts)
|
||||
if layout_page_count > 0 {
|
||||
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
|
||||
layout := controller.state.page_layouts[page_idx]
|
||||
if len(layout.panels) > 0 {
|
||||
panel_idx := clamp_bubble_cursor(len(layout.panels), summary_opts.bubble_panel_cursor)
|
||||
panel_id := layout.panels[panel_idx].panel_id
|
||||
msg := action_update_bubble(&controller, panel_id, summary_opts.bubble_edit_cursor, edited_type, "")
|
||||
push_status(&status_msg, &action_log, msg)
|
||||
is_dirty = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wheel := rl.GetMouseWheelMove()
|
||||
if wheel != 0 {
|
||||
layout_page_count := len(controller.state.page_layouts)
|
||||
if layout_page_count > 0 {
|
||||
summary_opts.bubble_panel_cursor -= int(wheel)
|
||||
if summary_opts.bubble_panel_cursor < 0 {
|
||||
summary_opts.bubble_panel_cursor = 0
|
||||
}
|
||||
panel_count := len(controller.state.page_layouts[summary_opts.bubble_page_cursor].panels)
|
||||
if summary_opts.bubble_panel_cursor >= panel_count {
|
||||
summary_opts.bubble_panel_cursor = panel_count - 1
|
||||
}
|
||||
summary_opts.bubble_edit_cursor = 0
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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")
|
||||
|
||||
@@ -176,8 +176,22 @@ draw_screen_summary :: proc(controller: ui.App_Controller, export_path: string,
|
||||
}
|
||||
}
|
||||
case .Bubbles:
|
||||
draw_summary_line(x, y+30, fmt.tprintf("Bubble maps: %d", len(controller.state.speech_bubbles)), rl.DARKGRAY)
|
||||
rl.DrawText("Bubble editor is scaffolded", x, y+54, 18, rl.DARKGRAY)
|
||||
bubble_count := 0
|
||||
if controller.state.speech_bubbles != nil {
|
||||
for _, bubbles in controller.state.speech_bubbles {
|
||||
bubble_count += len(bubbles)
|
||||
}
|
||||
}
|
||||
draw_summary_line(x, y+30, fmt.tprintf("Total bubbles: %d", bubble_count), rl.DARKGRAY)
|
||||
draw_summary_line(x, y+54, fmt.tprintf("Layout pages: %d", len(controller.state.page_layouts)), rl.DARKGRAY)
|
||||
if len(controller.state.page_layouts) == 0 {
|
||||
rl.DrawText("No layouts yet. Run Layout Auto first.", x, y+86, 18, SUMMARY_HINT)
|
||||
} else if bubble_count == 0 {
|
||||
rl.DrawText("No bubbles yet. Use Auto Place or Add on Bubbles screen.", x, y+86, 18, SUMMARY_HINT)
|
||||
} else {
|
||||
draw_summary_line(x, y+78, fmt.tprintf("Panels with bubbles: %d", len(controller.state.speech_bubbles)), SUMMARY_ACCENT)
|
||||
rl.DrawText("Select a panel in Bubbles screen to edit.", x, y+102, 18, SUMMARY_HINT)
|
||||
}
|
||||
case .Export:
|
||||
draw_summary_line(x, y+30, fmt.tprintf("Format: %v", controller.state.export_format), rl.DARKGRAY)
|
||||
draw_summary_line(x, y+54, fmt.tprintf("Layouts: %d | Panels: %d", len(controller.state.page_layouts), len(controller.state.panel_images)), rl.DARKGRAY)
|
||||
@@ -455,6 +469,62 @@ clamp_layout_cursor :: proc(layout_count, cursor: int) -> int {
|
||||
return cursor
|
||||
}
|
||||
|
||||
Layout_Validation_Result :: struct {
|
||||
coverage_pct: f32,
|
||||
missing_bindings: int,
|
||||
bounds_violations: int,
|
||||
total_panels: int,
|
||||
}
|
||||
|
||||
validate_layout_page :: proc(layout: core.Page_Layout, panel_images: map[string]core.Panel_Image) -> Layout_Validation_Result {
|
||||
result: Layout_Validation_Result
|
||||
result.total_panels = len(layout.panels)
|
||||
|
||||
// Coverage: sum of panel cell areas vs page area
|
||||
page_area := f32(layout.width) * f32(layout.height)
|
||||
covered_area: f32 = 0
|
||||
for p in layout.panels {
|
||||
cell_w := p.layout_cell.w * f32(layout.width)
|
||||
cell_h := p.layout_cell.h * f32(layout.height)
|
||||
covered_area += cell_w * cell_h
|
||||
}
|
||||
if page_area > 0 {
|
||||
result.coverage_pct = covered_area / page_area * 100
|
||||
}
|
||||
|
||||
// Missing bindings: panels without corresponding images
|
||||
for p in layout.panels {
|
||||
if _, has := panel_images[p.panel_id]; !has {
|
||||
result.missing_bindings += 1
|
||||
}
|
||||
}
|
||||
|
||||
// Bounds violations: cells that extend outside [0,1] range
|
||||
for p in layout.panels {
|
||||
c := p.layout_cell
|
||||
if c.x < 0 || c.y < 0 || c.x+c.w > 1.001 || c.y+c.h > 1.001 {
|
||||
result.bounds_violations += 1
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
draw_validation_badge :: proc(x, y, w: i32, label: string, ok: bool) {
|
||||
bg := UNREADY_BG
|
||||
border := UNREADY_BORDER
|
||||
fg := WARNING
|
||||
if ok {
|
||||
bg = READY_BG
|
||||
border = READY_BORDER
|
||||
fg = SUCCESS
|
||||
}
|
||||
rec := rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = 22}
|
||||
rl.DrawRectangleRounded(rec, RADIUS_CHIP, 6, bg)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_CHIP, 6, 1.0, border)
|
||||
draw_text_fitted(label, x+8, y+5, 11, int(w-16), 6, fg)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -473,6 +543,15 @@ draw_layout_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32,
|
||||
// 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)
|
||||
|
||||
// Validation badges
|
||||
val := validate_layout_page(layout, controller.state.panel_images)
|
||||
coverage_ok := val.coverage_pct >= 80 && val.coverage_pct <= 105
|
||||
bindings_ok := val.missing_bindings == 0
|
||||
bounds_ok := val.bounds_violations == 0
|
||||
draw_validation_badge(x+18, y+68, 100, fmt.tprintf("Cov: %.0f%%", val.coverage_pct), coverage_ok)
|
||||
draw_validation_badge(x+124, y+68, 110, fmt.tprintf("Bind: %d miss", val.missing_bindings), bindings_ok)
|
||||
draw_validation_badge(x+240, y+68, 100, fmt.tprintf("Bounds: %d", val.bounds_violations), bounds_ok)
|
||||
|
||||
// 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)
|
||||
@@ -481,11 +560,11 @@ draw_layout_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32,
|
||||
}
|
||||
|
||||
// Layout dimensions
|
||||
draw_summary_subline(x+18, y+68, fmt.tprintf("size: %d x %d", layout.width, layout.height), SUMMARY_SUBLINE)
|
||||
draw_summary_subline(x+18, y+94, fmt.tprintf("size: %d x %d", layout.width, layout.height), SUMMARY_SUBLINE)
|
||||
|
||||
// ── Mini wireframe preview ─────────────────────────────────
|
||||
preview_x := x + 18
|
||||
preview_y := y + 88
|
||||
preview_y := y + 114
|
||||
preview_max_w: f32 = f32(w) * 0.4
|
||||
preview_max_h: f32 = f32(h - 100)
|
||||
if preview_max_h < 40 {
|
||||
@@ -533,10 +612,10 @@ draw_layout_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32,
|
||||
|
||||
// ── Page list (right side) ─────────────────────────────────
|
||||
list_x := x + i32(preview_max_w) + 36
|
||||
list_y := y + 88
|
||||
list_y := y + 114
|
||||
list_w := w - i32(preview_max_w) - 54
|
||||
row_h: i32 = 18
|
||||
rows: i32 = (h - 100) / row_h
|
||||
rows: i32 = (h - 126) / row_h
|
||||
if rows < 1 {
|
||||
rows = 1
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ Summary_View_Options :: struct {
|
||||
layout_show_all: bool,
|
||||
layout_desc: bool,
|
||||
layout_page_cursor: int,
|
||||
bubble_page_cursor: int,
|
||||
bubble_panel_cursor: int,
|
||||
bubble_edit_cursor: int,
|
||||
}
|
||||
|
||||
Pending_Confirm_Action :: enum {
|
||||
|
||||
Reference in new issue
Block a user