From 9cc72e80412b9260a2b5d31d3778a765be7110d8 Mon Sep 17 00:00:00 2001 From: Sun Serega Date: Thu, 28 Dec 2023 19:48:50 +0200 Subject: [PATCH] camera copy/paste and save on exit --- Samples/OpenGLABC/Mandelbrot/.gitignore | 1 + Samples/OpenGLABC/Mandelbrot/0Mandelbrot.pas | 261 +++++++++++++++++- Samples/OpenGLABC/Mandelbrot/CameraDef.pas | 101 +++++++ .../OpenGLABC/Mandelbrot/PointComponents.pas | 40 +++ 4 files changed, 398 insertions(+), 5 deletions(-) diff --git a/Samples/OpenGLABC/Mandelbrot/.gitignore b/Samples/OpenGLABC/Mandelbrot/.gitignore index ee813f11..b758941a 100644 --- a/Samples/OpenGLABC/Mandelbrot/.gitignore +++ b/Samples/OpenGLABC/Mandelbrot/.gitignore @@ -2,5 +2,6 @@ Cache/block_w_pow=*/*.point_block +/camera.dat diff --git a/Samples/OpenGLABC/Mandelbrot/0Mandelbrot.pas b/Samples/OpenGLABC/Mandelbrot/0Mandelbrot.pas index 0f7b8637..4db2624a 100644 --- a/Samples/OpenGLABC/Mandelbrot/0Mandelbrot.pas +++ b/Samples/OpenGLABC/Mandelbrot/0Mandelbrot.pas @@ -5,14 +5,15 @@ // Управление: // - Escape: Завершение программы (дважды или +Ctrl чтобы отменить сохранение) -// - Space: Сбросить положение камеры // - Mouse Drag: Быстрое движение камеры // - Arrows: Гладкое движение камеры // - Scroll: Быстрое изменение масштаба // - "+" и "-": Гладкое изменение масштаба -//TODO: +// - Space: Сбросить положение камеры // - Ctrl+C: Скопировать положение камеры (+Shift чтобы добавить комментарий) // - Ctrl+V: Вставить положение камеры +// - Win+V: Вставить положение камеры из истории буфера обмена +//TODO: // - Alt: Вид без копирования информации предыдущих кадров // - Alt+Enter: Полноэкранный режим // - B: Телепортировать камеру к курсору (Blink) @@ -161,12 +162,16 @@ BoundUniforms = record // f.ClientSize := new System.Drawing.Size(1206,603); // 603p, 2:1 f.StartPosition := FormStartPosition.CenterScreen; + var camera_saved_pos_fname := 'camera.dat'; + var camera_saved_pos_enc := new System.Text.UTF8Encoding(true); + {$region Закрытие} f.KeyUp += (o,e)-> case e.KeyCode of Keys.Escape: f.Close; end; + var wait_for_last_frame := true; f.Closing += (o,e)-> begin if Control.ModifierKeys.HasFlag(Keys.Control) then Halt; @@ -174,7 +179,11 @@ BoundUniforms = record var shutdown_progress_form := new Form; shutdown_progress_form.StartPosition := FormStartPosition.CenterScreen; shutdown_progress_form.FormBorderStyle := FormBorderStyle.None; - shutdown_progress_form.Closing += (o,e)->Halt(); + shutdown_progress_form.Closing += (o,e)-> + begin + while wait_for_last_frame do ; + Halt; + end; shutdown_progress_form.KeyUp += (o,e)-> case e.KeyCode of Keys.Escape: shutdown_progress_form.Close; @@ -220,9 +229,123 @@ BoundUniforms = record end; end; + {$region speak} + + var speak: string->(); + try + {$reference System.Speech.dll} + + var all_voices := System.Speech.Synthesis.SpeechSynthesizer.Create.GetInstalledVoices; + if all_voices.Count=0 then raise new System.Exception('No installed voices'); + + var speaker_per_voice := all_voices.ToDictionary(v->v, v-> + begin + Result := new System.Speech.Synthesis.SpeechSynthesizer; + Result.SetOutputToDefaultAudioDevice; + Result.SelectVoice(v.VoiceInfo.Name); + if Result.Voice<>v.VoiceInfo then + raise new System.InvalidOperationException; + end); + + var speaker_per_letter := new Dictionary; + var add_letter_voice := procedure(ch: char; v: System.Speech.Synthesis.InstalledVoice)-> + begin + var ll := ch.ToLower; + var lu := ch.ToUpper; + if ll=lu then raise new System.InvalidOperationException; + var speaker := speaker_per_voice[v]; + speaker_per_letter.Add(ll, speaker); + speaker_per_letter.Add(lu, speaker); + end; + + var en_voice := all_voices.FirstOrDefault(v->'en' in v.VoiceInfo.Culture.Name); + if en_voice=nil then + $'No en voice!'.Println else + for var ch := 'a' to 'z' do + add_letter_voice(ch, en_voice); + + var ru_voice := all_voices.FirstOrDefault(v->'ru' in v.VoiceInfo.Culture.Name); + if ru_voice=nil then + $'No ru voice!'.Println else + begin + for var ch := 'а' to 'я' do + add_letter_voice(ch, ru_voice); + add_letter_voice('ё', ru_voice); + end; + + var def_voice := en_voice ?? ru_voice ?? all_voices.First; + var def_speaker := speaker_per_voice[def_voice]; + + speak := s->System.Threading.Tasks.Task.Run(()-> + try + foreach var sp in speaker_per_voice.Values do + sp.SpeakAsyncCancelAll; + + var sb := new StringBuilder(s.Length); + var speaker := default(System.Speech.Synthesis.SpeechSynthesizer); + var dump := ()-> + begin + if sb.Length=0 then exit; +// $'{speaker.Voice.Culture.Name} says [{sb}]'.Println; + speaker.Speak(sb.ToString); + sb.Clear; + end; + var add_char := procedure(ch: char; new_speaker: System.Speech.Synthesis.SpeechSynthesizer)-> + begin + if speaker<>new_speaker then dump; + speaker := new_speaker; + sb += ch; + end; + + var sb_neutral := new StringBuilder(s.Length); + foreach var ch in s do + begin + var new_speaker := speaker_per_letter.Get(ch); +// $'[{ch}: {new_speaker?.Voice.Culture.Name??''nil''}]'.Println; + if new_speaker=nil then + begin + sb_neutral.Append(ch); + continue; + end; + + if speaker in |nil,new_speaker| then + begin + speaker := new_speaker; + sb.Append(sb_neutral); + sb_neutral.Clear; + end else + begin + for var i := 0 to sb_neutral.Length-1 do + add_char(sb_neutral[i], def_speaker); + sb_neutral.Clear; + end; + + add_char(ch, new_speaker); + end; + if speaker=nil then + speaker := def_speaker; + sb.Append(sb_neutral); + + dump; + except + on System.OperationCanceledException do + ; + on e: Exception do + MessageBox.Show(e.ToString, 'Error speaking'); + end); + + except + on e: Exception do + Println('Failed to init TTS:', e); + end; + + {$endregion speak} + + var copy_camera_pos := default(Tuple); + var paste_camera_pos := default(Tuple); + var mouse_captured := true; var draw_alt_mode := false; var mouse_pos := default(Vec2i); - var mouse_captured := true; var mouse_grab_move := default(Vec2i); var scale_speed_add := 0; var camera_reset := false; @@ -232,12 +355,101 @@ BoundUniforms = record {$region Управление} begin + {$region copy/paste} + + if FileExists(camera_saved_pos_fname) then + try + paste_camera_pos := Tuple.Create(CameraPos.Parse(ReadAllText(camera_saved_pos_fname), speak)); + except + on e: Exception do + MessageBox.Show(e.ToString, $'Failed to load camera position'); + end; + + f.KeyUp += (o,e)->if e.Modifiers.HasFlag(Keys.Control) then + case e.KeyCode of + + Keys.C: + begin + var copy_comment := default(string); + if e.Modifiers.HasFlag(Keys.Shift) then + begin + var comment_inp_form := new Form; + comment_inp_form.Text := 'Comment'; + comment_inp_form.FormBorderStyle := FormBorderStyle.FixedSingle; + comment_inp_form.StartPosition := FormStartPosition.CenterParent; + + var comment_inp_tb := new RichTextBox; + comment_inp_form.Controls.Add(comment_inp_tb); + comment_inp_tb.Dock := DockStyle.Fill; + + var reset_form_size := ()-> + begin + var text_size := comment_inp_form.CreateGraphics.MeasureString(comment_inp_tb.Text+'a', comment_inp_tb.Font); + comment_inp_form.ClientSize := new System.Drawing.Size( + Ceil(text_size.Width+10).ClampBottom(250), + Ceil(text_size.Height+10).ClampBottom(30) + ); + var screen_size := Screen.FromControl(comment_inp_form).WorkingArea.Size; + comment_inp_form.Location := new System.Drawing.Point( + (screen_size.Width-comment_inp_form.Width) div 2, + (screen_size.Height-comment_inp_form.Height) div 2 + ); + end; + reset_form_size; + comment_inp_tb.TextChanged += (o,e)->reset_form_size(); + + comment_inp_tb.KeyUp += (o,e)-> + case e.KeyCode of + + Keys.Escape: + comment_inp_form.Close; + + Keys.Enter: + if not e.Modifiers.HasFlag(Keys.Shift) then + begin + copy_comment := comment_inp_tb.Text.Replace(#13#10,#10); + copy_comment := copy_comment.Remove(copy_comment.Length-1); + comment_inp_form.Close; + end; + + end; + + comment_inp_form.ShowDialog; + if copy_comment=nil then exit; + end; + copy_camera_pos := Tuple.Create(copy_comment); + end; + + Keys.V: + try + paste_camera_pos := Tuple.Create( + CameraPos.Parse(Clipboard.GetText, speak) + ); + except + on ex: Exception do + MessageBox.Show(ex.ToString, 'Failed to parse camera position'); + end; + + end; + + {$endregion copy/paste} + + {$region mouse_captured} + f.MouseEnter += (o,e)->(mouse_captured := true); f.MouseLeave += (o,e)->(mouse_captured := false); + {$endregion mouse_captured} + + {$region draw_alt_mode} + f.KeyDown += (o,e)->(draw_alt_mode := e.Alt); f.KeyUp += (o,e)->(draw_alt_mode := e.Alt); + {$endregion draw_alt_mode} + + {$region camera reset} + f.KeyDown += (o,e)-> case e.KeyCode of Keys.Space: @@ -247,6 +459,10 @@ BoundUniforms = record end; end; + {$endregion camera reset} + + {$region camera drag} + var mouse_grabbed := false; f.MouseDown += (o,e)-> case e.Button of @@ -257,7 +473,6 @@ BoundUniforms = record MouseButtons.Left: mouse_grabbed := false; end; - f.MouseWheel += (o,e)->System.Threading.Interlocked.Add(scale_speed_add, e.Delta); f.MouseMove += (o,e)-> begin var n_mouse_pos := new Vec2i(e.X,e.Y); @@ -270,6 +485,16 @@ BoundUniforms = record mouse_pos := n_mouse_pos; end; + {$endregion camera drag} + + {$region camera scroll} + + f.MouseWheel += (o,e)->System.Threading.Interlocked.Add(scale_speed_add, e.Delta); + + {$endregion camera scroll} + + {$region camera slow control} + var define_slow_control := procedure(key_low, key_high, modifiers: Keys; on_change: integer->())-> begin var low_pressed := false; @@ -299,6 +524,8 @@ BoundUniforms = record define_slow_control(Keys.Left, Keys.Right, Keys.None, x->(slow_move_dir.val0:=x)); define_slow_control(Keys.Down, Keys.Up, Keys.None, x->(slow_move_dir.val1:=x)); + {$endregion camera slow control} + end; {$endregion Управление} @@ -382,6 +609,28 @@ BoundUniforms = record end; t_body.Start; + begin + var l_copy_camera_pos := System.Threading.Interlocked.Exchange(copy_camera_pos, nil); + var n_camera := camera; + if l_copy_camera_pos<>nil then f.BeginInvoke(()-> + begin + Clipboard.SetText(n_camera.ToString(l_copy_camera_pos.Item1)); + Console.Beep; + end); + end; + + begin + var l_paste_camera_pos := System.Threading.Interlocked.Exchange(paste_camera_pos, nil); + if l_paste_camera_pos<>nil then + begin + var n_camera := l_paste_camera_pos.Item1; + n_camera.dw := camera.dw; + n_camera.dh := camera.dh; + camera := n_camera; + scale_speed := 0; + end; + end; + begin var next_frame_time := frame_time_sw.Elapsed; var frame_len := (next_frame_time-last_frame_time).TotalSeconds; @@ -530,6 +779,8 @@ BoundUniforms = record Println(e); end; + WriteAllText(camera_saved_pos_fname, camera.ToString, camera_saved_pos_enc); + wait_for_last_frame := false; except on e: Exception do begin diff --git a/Samples/OpenGLABC/Mandelbrot/CameraDef.pas b/Samples/OpenGLABC/Mandelbrot/CameraDef.pas index a21c6e68..741dd3a8 100644 --- a/Samples/OpenGLABC/Mandelbrot/CameraDef.pas +++ b/Samples/OpenGLABC/Mandelbrot/CameraDef.pas @@ -97,6 +97,107 @@ CameraPos = record FixScalePow; end; + public function ToString(comment: string): string; + begin + var sb := new StringBuilder; + + if comment<>nil then + begin + sb += 'c='; + sb += comment.Replace('\','\\').Replace(#13#10,#10).Replace(#10,'\'#10); + sb += #10; + end; + + sb += 'pos.r='; + sb += self.pos.r.ToString; + sb += #10; + + sb += 'pos.i='; + sb += self.pos.i.ToString; + sb += #10; + + sb += 'scale='; + sb += self.scale_fine.ToString; + sb += '*2^'; + sb += self.scale_pow.ToString; + + Result := sb.ToString; + end; + public function ToString: string; override := ToString(nil); + + public static function Parse(text: string; on_comment: string->()): CameraPos; + begin + Result := default(CameraPos); + var scale_is_set := false; + + var l_enmr := text.Replace(#13#10,#10).Split(#10).AsEnumerable.GetEnumerator; + while l_enmr.MoveNext do + begin + var l := l_enmr.Current; + if string.IsNullOrWhiteSpace(l) then continue; + var spl := l.Split(|'='|,2); + if spl.Length<>2 then + raise new System.FormatException(l); + case spl[0] of + + 'pos.r': + if Result.pos.r.Words=nil then + Result.pos.r := PointComponent.Parse(spl[1]) else + raise new System.FormatException($'pos.r is already set: [{#10}{text}{#10}]'); + 'pos.i': + if Result.pos.i.Words=nil then + Result.pos.i := PointComponent.Parse(spl[1]) else + raise new System.FormatException($'pos.i is already set: [{#10}{text}{#10}]'); + + 'scale': + begin + if scale_is_set then + raise new System.FormatException($'scale is already set: [{#10}{text}{#10}]'); + spl := spl[1].Split(|'*2^'|,2,System.StringSplitOptions.None); + Result.scale_fine := single.Parse(spl[0]); + Result.scale_pow := integer.Parse(spl[1]); + scale_is_set := true; + end; + + 'c': + begin + var sb := new StringBuilder(spl[1]); + while true do + begin + + begin + var last_bs_c := 0; + while last_bs_c < sb.Length do + begin + if sb[sb.Length-1-last_bs_c] <> '\' then break; + last_bs_c += 1; + end; + if last_bs_c.IsEven then break; + end; + + sb.Length -= 1; + if not l_enmr.MoveNext then + raise new System.FormatException('Unfinished comment'); + sb += #10; + sb += l_enmr.Current; + + end; + + if on_comment<>nil then on_comment(sb.ToString.Replace('\\','\')); + end; + + end; + end; + + if Result.pos.r.Words=nil then + raise new System.FormatException($'pos.r not set: [{#10}{text}{#10}]'); + if Result.pos.i.Words=nil then + raise new System.FormatException($'pos.i not set: [{#10}{text}{#10}]'); + if not scale_is_set then + raise new System.FormatException($'scale not set: [{#10}{text}{#10}]'); + + end; + end; end. \ No newline at end of file diff --git a/Samples/OpenGLABC/Mandelbrot/PointComponents.pas b/Samples/OpenGLABC/Mandelbrot/PointComponents.pas index b0ecfbce..5f4478d9 100644 --- a/Samples/OpenGLABC/Mandelbrot/PointComponents.pas +++ b/Samples/OpenGLABC/Mandelbrot/PointComponents.pas @@ -1,5 +1,7 @@ unit PointComponents; +{$zerobasedstrings} + {$savepcu false} //TODO uses Settings; @@ -174,6 +176,44 @@ PointComponent = record(System.IEquatable) {$endif DEBUG} Result := sb.ToString; end; + public static function Parse(s: string): PointComponent; + begin + Result._words := s.Split('|').ConvertAll((w,i)-> + begin + var raise_format := procedure(m: string)-> + raise new System.FormatException($'{m} in word#{i} [{w}] of [{s}]'); + + if i=0 then + begin + + case w[0] of + '+': w[0] := '0'; + '-': w[0] := '1'; + else raise_format($'First char of first word must be a sign'); + end; + + if w[Settings.z_int_bits]<>'.' then + raise_format($'Char#{Settings.z_int_bits} of first word must be a decimal point'); + w := w.Remove(Settings.z_int_bits,1); + + end; + + if w.Length<>32 then + raise_format($'Word must be 32 chars long, got {w.Length}'); + + Result := cardinal(0); + foreach var ch in w index ch_ind do + begin + Result *= 2; + case ch of + '0': ; + '1': Result += 1; + else raise_format($'Char#{ch_ind} was [{ch}]'); + end; + end; + + end); + end; public procedure Save(bw: System.IO.BinaryWriter) := foreach var x in Words do bw.Write(x);