teek 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +46 -0
  3. data/Rakefile +161 -5
  4. data/ext/teek/extconf.rb +1 -1
  5. data/ext/teek/tcltkbridge.c +3 -0
  6. data/ext/teek/tcltkbridge.h +3 -0
  7. data/ext/teek/tkeventsource.c +195 -0
  8. data/ext/teek/tkphoto.c +169 -5
  9. data/ext/teek/tkwin.c +84 -0
  10. data/lib/teek/background_ractor4x.rb +32 -4
  11. data/lib/teek/photo.rb +232 -0
  12. data/lib/teek/version.rb +1 -1
  13. data/lib/teek.rb +3 -1
  14. data/sample/optcarrot/vendor/optcarrot/apu.rb +856 -0
  15. data/sample/optcarrot/vendor/optcarrot/config.rb +257 -0
  16. data/sample/optcarrot/vendor/optcarrot/cpu.rb +1162 -0
  17. data/sample/optcarrot/vendor/optcarrot/driver.rb +144 -0
  18. data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +14 -0
  19. data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +105 -0
  20. data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +153 -0
  21. data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +14 -0
  22. data/sample/optcarrot/vendor/optcarrot/nes.rb +105 -0
  23. data/sample/optcarrot/vendor/optcarrot/opt.rb +168 -0
  24. data/sample/optcarrot/vendor/optcarrot/pad.rb +92 -0
  25. data/sample/optcarrot/vendor/optcarrot/palette.rb +65 -0
  26. data/sample/optcarrot/vendor/optcarrot/ppu.rb +1468 -0
  27. data/sample/optcarrot/vendor/optcarrot/rom.rb +143 -0
  28. data/sample/optcarrot/vendor/optcarrot.rb +14 -0
  29. data/sample/optcarrot.rb +354 -0
  30. data/sample/paint/assets/bucket.png +0 -0
  31. data/sample/paint/assets/cursor.png +0 -0
  32. data/sample/paint/assets/eraser.png +0 -0
  33. data/sample/paint/assets/pencil.png +0 -0
  34. data/sample/paint/assets/spray.png +0 -0
  35. data/sample/paint/layer.rb +255 -0
  36. data/sample/paint/layer_manager.rb +179 -0
  37. data/sample/paint/paint_demo.rb +837 -0
  38. data/sample/paint/sparse_pixel_buffer.rb +202 -0
  39. data/sample/sdl2_demo.rb +318 -0
  40. metadata +29 -1
data/ext/teek/tkphoto.c CHANGED
@@ -1,4 +1,4 @@
1
- /* tkphoto.c - Photo image C functions for tk-ng
1
+ /* tkphoto.c - Photo image C functions for teek
2
2
  *
3
3
  * Fast pixel manipulation using Tk's photo image C API.
4
4
  * These functions bypass Tcl string parsing for better performance.
@@ -18,8 +18,9 @@
18
18
  * width - Image width in pixels
19
19
  * height - Image height in pixels
20
20
  * opts - Optional hash:
21
- * :x, :y - destination offsets (default 0,0)
22
- * :format - :rgba (default) or :argb
21
+ * :x, :y - destination offsets (default 0,0)
22
+ * :format - :rgba (default) or :argb
23
+ * :composite - :set (default, overwrite) or :overlay (alpha blend)
23
24
  *
24
25
  * The pixel_data must be exactly width * height * 4 bytes.
25
26
  *
@@ -38,6 +39,7 @@ interp_photo_put_block(int argc, VALUE *argv, VALUE self)
38
39
  Tk_PhotoImageBlock block;
39
40
  int width, height, x_off, y_off;
40
41
  int is_argb = 0;
42
+ int comp_rule = TK_PHOTO_COMPOSITE_SET;
41
43
  long expected_size;
42
44
 
43
45
  rb_scan_args(argc, argv, "41", &photo_path, &pixel_data, &width_val, &height_val, &opts);
@@ -74,6 +76,12 @@ interp_photo_put_block(int argc, VALUE *argv, VALUE self)
74
76
  is_argb = 1;
75
77
  }
76
78
  }
79
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("composite")));
80
+ if (!NIL_P(val) && TYPE(val) == T_SYMBOL) {
81
+ if (rb_intern("overlay") == SYM2ID(val)) {
82
+ comp_rule = TK_PHOTO_COMPOSITE_OVERLAY;
83
+ }
84
+ }
77
85
  }
78
86
 
79
87
  /* Find the photo image by Tcl path */
@@ -105,7 +113,7 @@ interp_photo_put_block(int argc, VALUE *argv, VALUE self)
105
113
 
106
114
  /* Write pixels to the photo image */
107
115
  if (Tk_PhotoPutBlock(tip->interp, photo, &block, x_off, y_off,
108
- width, height, TK_PHOTO_COMPOSITE_SET) != TCL_OK) {
116
+ width, height, comp_rule) != TCL_OK) {
109
117
  rb_raise(eTclError, "Tk_PhotoPutBlock failed: %s",
110
118
  Tcl_GetStringResult(tip->interp));
111
119
  }
@@ -129,6 +137,7 @@ interp_photo_put_block(int argc, VALUE *argv, VALUE self)
129
137
  * :zoom_x, :zoom_y - zoom factors (default 1,1)
130
138
  * :subsample_x, :subsample_y - subsample factors (default 1,1)
131
139
  * :format - :rgba (default) or :argb
140
+ * :composite - :set (default, overwrite) or :overlay (alpha blend)
132
141
  *
133
142
  * The pixel_data must be exactly width * height * 4 bytes.
134
143
  * Zoom replicates pixels (zoom=3 makes each pixel 3x3).
@@ -151,6 +160,7 @@ interp_photo_put_zoomed_block(int argc, VALUE *argv, VALUE self)
151
160
  int zoom_x, zoom_y, subsample_x, subsample_y;
152
161
  int dest_width, dest_height;
153
162
  int is_argb = 0;
163
+ int comp_rule = TK_PHOTO_COMPOSITE_SET;
154
164
  long expected_size;
155
165
 
156
166
  rb_scan_args(argc, argv, "41", &photo_path, &pixel_data, &width_val, &height_val, &opts);
@@ -200,6 +210,12 @@ interp_photo_put_zoomed_block(int argc, VALUE *argv, VALUE self)
200
210
  is_argb = 1;
201
211
  }
202
212
  }
213
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("composite")));
214
+ if (!NIL_P(val) && TYPE(val) == T_SYMBOL) {
215
+ if (rb_intern("overlay") == SYM2ID(val)) {
216
+ comp_rule = TK_PHOTO_COMPOSITE_OVERLAY;
217
+ }
218
+ }
203
219
  }
204
220
 
205
221
  /* Validate zoom/subsample */
@@ -245,7 +261,7 @@ interp_photo_put_zoomed_block(int argc, VALUE *argv, VALUE self)
245
261
  if (Tk_PhotoPutZoomedBlock(tip->interp, photo, &block, x_off, y_off,
246
262
  dest_width, dest_height,
247
263
  zoom_x, zoom_y, subsample_x, subsample_y,
248
- TK_PHOTO_COMPOSITE_SET) != TCL_OK) {
264
+ comp_rule) != TCL_OK) {
249
265
  rb_raise(eTclError, "Tk_PhotoPutZoomedBlock failed: %s",
250
266
  Tcl_GetStringResult(tip->interp));
251
267
  }
@@ -460,6 +476,151 @@ interp_photo_blank(VALUE self, VALUE photo_path)
460
476
  return Qnil;
461
477
  }
462
478
 
479
+ /* ---------------------------------------------------------
480
+ * Interp#photo_set_size(photo_path, width, height)
481
+ *
482
+ * Set the dimensions of a photo image using Tk_PhotoSetSize.
483
+ *
484
+ * Arguments:
485
+ * photo_path - Tcl path of the photo image
486
+ * width - New width in pixels
487
+ * height - New height in pixels
488
+ *
489
+ * Returns nil.
490
+ *
491
+ * See: https://www.tcl-lang.org/man/tcl8.6/TkLib/FindPhoto.htm
492
+ * --------------------------------------------------------- */
493
+
494
+ static VALUE
495
+ interp_photo_set_size(VALUE self, VALUE photo_path, VALUE width_val, VALUE height_val)
496
+ {
497
+ struct tcltk_interp *tip = get_interp(self);
498
+ Tk_PhotoHandle photo;
499
+ int width, height;
500
+
501
+ StringValue(photo_path);
502
+ width = NUM2INT(width_val);
503
+ height = NUM2INT(height_val);
504
+
505
+ if (width < 0 || height < 0) {
506
+ rb_raise(rb_eArgError, "width and height must be non-negative");
507
+ }
508
+
509
+ photo = Tk_FindPhoto(tip->interp, StringValueCStr(photo_path));
510
+ if (!photo) {
511
+ rb_raise(eTclError, "photo image not found: %s", StringValueCStr(photo_path));
512
+ }
513
+
514
+ if (Tk_PhotoSetSize(tip->interp, photo, width, height) != TCL_OK) {
515
+ rb_raise(eTclError, "Tk_PhotoSetSize failed: %s",
516
+ Tcl_GetStringResult(tip->interp));
517
+ }
518
+
519
+ return Qnil;
520
+ }
521
+
522
+ /* ---------------------------------------------------------
523
+ * Interp#photo_expand(photo_path, width, height)
524
+ *
525
+ * Expand a photo image to at least the given dimensions using Tk_PhotoExpand.
526
+ * Will not shrink the image if it is already larger.
527
+ *
528
+ * Arguments:
529
+ * photo_path - Tcl path of the photo image
530
+ * width - Minimum width in pixels
531
+ * height - Minimum height in pixels
532
+ *
533
+ * Returns nil.
534
+ *
535
+ * See: https://www.tcl-lang.org/man/tcl8.6/TkLib/FindPhoto.htm
536
+ * --------------------------------------------------------- */
537
+
538
+ static VALUE
539
+ interp_photo_expand(VALUE self, VALUE photo_path, VALUE width_val, VALUE height_val)
540
+ {
541
+ struct tcltk_interp *tip = get_interp(self);
542
+ Tk_PhotoHandle photo;
543
+ int width, height;
544
+
545
+ StringValue(photo_path);
546
+ width = NUM2INT(width_val);
547
+ height = NUM2INT(height_val);
548
+
549
+ if (width < 0 || height < 0) {
550
+ rb_raise(rb_eArgError, "width and height must be non-negative");
551
+ }
552
+
553
+ photo = Tk_FindPhoto(tip->interp, StringValueCStr(photo_path));
554
+ if (!photo) {
555
+ rb_raise(eTclError, "photo image not found: %s", StringValueCStr(photo_path));
556
+ }
557
+
558
+ if (Tk_PhotoExpand(tip->interp, photo, width, height) != TCL_OK) {
559
+ rb_raise(eTclError, "Tk_PhotoExpand failed: %s",
560
+ Tcl_GetStringResult(tip->interp));
561
+ }
562
+
563
+ return Qnil;
564
+ }
565
+
566
+ /* ---------------------------------------------------------
567
+ * Interp#photo_get_pixel(photo_path, x, y)
568
+ *
569
+ * Read a single pixel from a photo image using Tk_PhotoGetImage.
570
+ * Faster than going through Tcl's "$photo get x y" string parsing.
571
+ *
572
+ * Arguments:
573
+ * photo_path - Tcl path of the photo image
574
+ * x - X coordinate
575
+ * y - Y coordinate
576
+ *
577
+ * Returns [r, g, b, a] array.
578
+ *
579
+ * See: https://www.tcl-lang.org/man/tcl8.6/TkLib/FindPhoto.htm
580
+ * --------------------------------------------------------- */
581
+
582
+ static VALUE
583
+ interp_photo_get_pixel(VALUE self, VALUE photo_path, VALUE x_val, VALUE y_val)
584
+ {
585
+ struct tcltk_interp *tip = get_interp(self);
586
+ Tk_PhotoHandle photo;
587
+ Tk_PhotoImageBlock block;
588
+ int x, y;
589
+ unsigned char *src;
590
+ int r_off, g_off, b_off, a_off;
591
+
592
+ StringValue(photo_path);
593
+ x = NUM2INT(x_val);
594
+ y = NUM2INT(y_val);
595
+
596
+ photo = Tk_FindPhoto(tip->interp, StringValueCStr(photo_path));
597
+ if (!photo) {
598
+ rb_raise(eTclError, "photo image not found: %s", StringValueCStr(photo_path));
599
+ }
600
+
601
+ if (!Tk_PhotoGetImage(photo, &block)) {
602
+ rb_raise(eTclError, "failed to get photo image data");
603
+ }
604
+
605
+ if (x < 0 || x >= block.width || y < 0 || y >= block.height) {
606
+ rb_raise(rb_eArgError, "coordinates (%d, %d) outside image bounds (%d x %d)",
607
+ x, y, block.width, block.height);
608
+ }
609
+
610
+ r_off = block.offset[0];
611
+ g_off = block.offset[1];
612
+ b_off = block.offset[2];
613
+ a_off = block.offset[3];
614
+
615
+ src = block.pixelPtr + y * block.pitch + x * block.pixelSize;
616
+
617
+ return rb_ary_new_from_args(4,
618
+ INT2FIX(src[r_off]),
619
+ INT2FIX(src[g_off]),
620
+ INT2FIX(src[b_off]),
621
+ INT2FIX((block.pixelSize >= 4) ? src[a_off] : 255));
622
+ }
623
+
463
624
  /* ---------------------------------------------------------
464
625
  * Init_tkphoto - Register photo image methods on Teek::Interp class
465
626
  *
@@ -473,5 +634,8 @@ Init_tkphoto(VALUE cInterp)
473
634
  rb_define_method(cInterp, "photo_put_zoomed_block", interp_photo_put_zoomed_block, -1);
474
635
  rb_define_method(cInterp, "photo_get_image", interp_photo_get_image, -1);
475
636
  rb_define_method(cInterp, "photo_get_size", interp_photo_get_size, 1);
637
+ rb_define_method(cInterp, "photo_set_size", interp_photo_set_size, 3);
638
+ rb_define_method(cInterp, "photo_expand", interp_photo_expand, 3);
639
+ rb_define_method(cInterp, "photo_get_pixel", interp_photo_get_pixel, 3);
476
640
  rb_define_method(cInterp, "photo_blank", interp_photo_blank, 1);
477
641
  }
data/ext/teek/tkwin.c CHANGED
@@ -5,6 +5,21 @@
5
5
  */
6
6
 
7
7
  #include "tcltkbridge.h"
8
+ #include <stdint.h>
9
+
10
+ #ifdef __APPLE__
11
+ /*
12
+ * Platform stubs for macOS. Tk_InitStubs sets up tkStubsPtr but
13
+ * tkPlatStubsPtr needs to be pulled from the hooks table.
14
+ * MAC_OSX_TK is needed for tkPlatDecls.h to expose macOS APIs.
15
+ */
16
+ #ifndef MAC_OSX_TK
17
+ #define MAC_OSX_TK
18
+ #endif
19
+ #include "tkPlatDecls.h"
20
+ #elif defined(_WIN32) || defined(__CYGWIN__)
21
+ #include "tkPlatDecls.h"
22
+ #endif
8
23
 
9
24
  /* ---------------------------------------------------------
10
25
  * Interp#user_inactive_time
@@ -129,6 +144,74 @@ interp_coords_to_window(VALUE self, VALUE root_x, VALUE root_y)
129
144
  return rb_utf8_str_new_cstr(pathName);
130
145
  }
131
146
 
147
+ /* ---------------------------------------------------------
148
+ * Interp#native_window_handle(window_path)
149
+ *
150
+ * Returns the platform-native window handle for SDL2 embedding.
151
+ *
152
+ * macOS: NSWindow* (via Tk_MacOSXGetNSWindowForDrawable)
153
+ * X11: X Window ID (via Tk_WindowId)
154
+ * Windows: HWND (via Tk_GetHWND)
155
+ *
156
+ * The return value is an Integer suitable for passing to
157
+ * SDL_CreateWindowFrom. On macOS this is a pointer to an
158
+ * NSWindow object; on X11/Windows it's a window identifier.
159
+ *
160
+ * The window must be mapped (visible) before calling this.
161
+ * Use `update_idletasks` first to ensure geometry is committed.
162
+ * --------------------------------------------------------- */
163
+
164
+ static VALUE
165
+ interp_native_window_handle(VALUE self, VALUE window_path)
166
+ {
167
+ struct tcltk_interp *tip = get_interp(self);
168
+ Tk_Window mainWin;
169
+ Tk_Window tkwin;
170
+ Drawable drawable;
171
+
172
+ StringValue(window_path);
173
+
174
+ mainWin = Tk_MainWindow(tip->interp);
175
+ if (!mainWin) {
176
+ rb_raise(eTclError, "Tk not initialized (no main window)");
177
+ }
178
+
179
+ tkwin = Tk_NameToWindow(tip->interp, StringValueCStr(window_path), mainWin);
180
+ if (!tkwin) {
181
+ rb_raise(eTclError, "window not found: %s", StringValueCStr(window_path));
182
+ }
183
+
184
+ /* Force the window to be mapped so we get a valid native handle */
185
+ Tk_MakeWindowExist(tkwin);
186
+
187
+ drawable = Tk_WindowId(tkwin);
188
+ if (!drawable) {
189
+ rb_raise(eTclError, "window has no native handle (not mapped?): %s",
190
+ StringValueCStr(window_path));
191
+ }
192
+
193
+ #ifdef __APPLE__
194
+ {
195
+ /* macOS: convert Tk drawable to NSWindow* for SDL2 */
196
+ void *nswindow = Tk_MacOSXGetNSWindowForDrawable(drawable);
197
+ if (!nswindow) {
198
+ rb_raise(eTclError, "could not get NSWindow for: %s",
199
+ StringValueCStr(window_path));
200
+ }
201
+ return ULL2NUM((uintptr_t)nswindow);
202
+ }
203
+ #elif defined(_WIN32) || defined(__CYGWIN__)
204
+ {
205
+ /* Windows: convert to HWND */
206
+ HWND hwnd = Tk_GetHWND(drawable);
207
+ return ULL2NUM((uintptr_t)hwnd);
208
+ }
209
+ #else
210
+ /* X11: Drawable is already the X Window ID */
211
+ return ULL2NUM((uintptr_t)drawable);
212
+ #endif
213
+ }
214
+
132
215
  /* ---------------------------------------------------------
133
216
  * Init_tkwin - Register Tk window query methods on Interp
134
217
  *
@@ -141,4 +224,5 @@ Init_tkwin(VALUE cInterp)
141
224
  rb_define_method(cInterp, "user_inactive_time", interp_user_inactive_time, 0);
142
225
  rb_define_method(cInterp, "get_root_coords", interp_get_root_coords, 1);
143
226
  rb_define_method(cInterp, "coords_to_window", interp_coords_to_window, 2);
227
+ rb_define_method(cInterp, "native_window_handle", interp_native_window_handle, 1);
144
228
  }
@@ -154,13 +154,20 @@ module Teek
154
154
  def close
155
155
  @done = true
156
156
  # Send stop to let the worker terminate itself — Ruby 4.x doesn't
157
- # allow closing a Ractor from outside.
157
+ # allow closing a Ractor from outside. The message thread will
158
+ # raise StopIteration on the Ractor's main thread, which triggers
159
+ # the rescue block that sends [:done] to the output port and exits
160
+ # the Ractor cleanly.
158
161
  begin
159
162
  @control_port&.send(:stop)
160
163
  rescue Ractor::ClosedError
161
164
  # Already closed
162
165
  end
163
166
  @control_port = nil
167
+ # Wait for the bridge thread to receive [:done] and exit. Without
168
+ # this, the zombie bridge thread blocks subsequent operations on
169
+ # Windows (Ractor::Port#receive holds the GVL).
170
+ @bridge_thread&.join(2)
164
171
  self
165
172
  end
166
173
 
@@ -174,7 +181,17 @@ module Teek
174
181
 
175
182
  # Wrap in isolated proc for Ractor sharing. The block can only access
176
183
  # its parameters (task, data), not outer-scope variables.
177
- shareable_block = Ractor.shareable_proc(&@work_block)
184
+ isolation_error = false
185
+ begin
186
+ shareable_block = Ractor.shareable_proc(&@work_block)
187
+ rescue Ractor::IsolationError
188
+ isolation_error = true
189
+ end
190
+ if isolation_error
191
+ raise Ractor::IsolationError,
192
+ "Background work block must not reference outside variables (including `app`). " \
193
+ "Use t.yield() to send results to on_progress, which runs on the main thread."
194
+ end
178
195
 
179
196
  start_ractor(shareable_block)
180
197
  start_polling
@@ -202,6 +219,7 @@ module Teek
202
219
  output_port = Ractor::Port.new
203
220
 
204
221
  @worker_ractor = Ractor.new(data, output_port, shareable_block) do |d, out, blk|
222
+ # :nocov: -- Coverage.so cannot observe execution inside a Ractor
205
223
  # Worker creates its own control port for receiving messages
206
224
  control_port = Ractor::Port.new
207
225
  msg_queue = Thread::Queue.new
@@ -209,13 +227,20 @@ module Teek
209
227
  # Send control port back to main thread
210
228
  out.send([:control_port, control_port])
211
229
 
212
- # Background thread receives from control port, forwards to queue
230
+ # Background thread receives from control port, forwards to queue.
231
+ # On :stop, interrupts the main Ractor thread with StopIteration
232
+ # (the main block may never call check_message) and signals the
233
+ # bridge thread via [:done] on the output port.
234
+ main_thread = Thread.current
213
235
  Thread.new do
214
236
  loop do
215
237
  begin
216
238
  msg = control_port.receive
217
239
  msg_queue << msg
218
- break if msg == :stop
240
+ if msg == :stop
241
+ main_thread.raise(StopIteration)
242
+ break
243
+ end
219
244
  rescue Ractor::ClosedError
220
245
  break
221
246
  end
@@ -233,6 +258,7 @@ module Teek
233
258
  out.send([:error, "#{e.class}: #{e.message}\n#{e.backtrace.first(3).join("\n")}"])
234
259
  out.send([:done])
235
260
  end
261
+ # :nocov:
236
262
  end
237
263
 
238
264
  # Bridge thread: Port.receive -> Queue
@@ -335,6 +361,7 @@ module Teek
335
361
  # Context object passed to the worker block inside the Ractor.
336
362
  # Provides methods for yielding results, sending/receiving messages,
337
363
  # and responding to pause/stop signals.
364
+ # :nocov: -- TaskContext runs exclusively inside a Ractor; invisible to Coverage.so
338
365
  class TaskContext
339
366
  # @api private
340
367
  def initialize(output_port, msg_queue)
@@ -406,6 +433,7 @@ module Teek
406
433
  end
407
434
  end
408
435
  end
436
+ # :nocov:
409
437
  end
410
438
  end
411
439
  end
data/lib/teek/photo.rb ADDED
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teek
4
+ # CPU-side RGBA pixel buffer backed by Tk's "photo image" format.
5
+ #
6
+ # Despite the name, this is really a raw pixel manipulation surface.
7
+ # Tk has two built-in image types: "bitmap" (two colors + transparency)
8
+ # and "photo" (full-color, 32-bit RGBA). The naming reflects Tk's
9
+ # image type system, not the contents — a "photo" is just Tk's term
10
+ # for a full-color pixel buffer.
11
+ #
12
+ # Think of it as a software framebuffer: you pack RGBA bytes, write
13
+ # them in bulk, read them back, zoom/subsample, and blit to a canvas
14
+ # or label for display. All work is CPU-driven — there is no GPU
15
+ # acceleration.
16
+ #
17
+ # @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/photo.html Tk photo image type
18
+ # @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/bitmap.html Tk bitmap image type
19
+ # @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/image.html Tk image command (lists all types)
20
+ #
21
+ # The C methods ({#put_block}, {#get_image}, {#get_pixel}, etc.) call
22
+ # Tk_PhotoPutBlock / Tk_PhotoGetImage directly, bypassing Tcl string
23
+ # parsing for much better performance than the Tcl-level +$photo put+
24
+ # command. Designed for games, visualizations, and real-time drawing.
25
+ #
26
+ # @example Create and fill with red pixels
27
+ # photo = Teek::Photo.new(app, width: 100, height: 100)
28
+ # red = ([255, 0, 0, 255].pack('CCCC')) * (100 * 100)
29
+ # photo.put_block(red, 100, 100)
30
+ #
31
+ # @example Read pixels back
32
+ # result = photo.get_image
33
+ # r, g, b, a = result[:data][0, 4].unpack('CCCC')
34
+ #
35
+ # @example Zoom a small sprite
36
+ # sprite = Teek::Photo.new(app, width: 64, height: 64)
37
+ # # ... fill sprite pixels ...
38
+ # dest = Teek::Photo.new(app, width: 192, height: 192)
39
+ # dest.put_zoomed_block(sprite_data, 64, 64, zoom_x: 3, zoom_y: 3)
40
+ #
41
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkLib/FindPhoto.htm Tk Photo C API
42
+ class Photo
43
+ attr_reader :app, :name
44
+
45
+ @counter = 0
46
+
47
+ class << self
48
+ # @api private
49
+ def next_name
50
+ @counter += 1
51
+ "teek_photo#{@counter}"
52
+ end
53
+ end
54
+
55
+ # Create a new photo image.
56
+ #
57
+ # @param app [Teek::App] the application instance
58
+ # @param name [String, nil] Tcl image name (auto-generated if nil)
59
+ # @param width [Integer, nil] image width in pixels
60
+ # @param height [Integer, nil] image height in pixels
61
+ # @param file [String, nil] path to an image file to load
62
+ # @param data [String, nil] base64-encoded image data
63
+ # @param format [String, nil] image format (e.g. "png", "gif")
64
+ # @param palette [String, nil] palette specification
65
+ # @param gamma [Float, nil] gamma correction value
66
+ def initialize(app, name: nil, width: nil, height: nil,
67
+ file: nil, data: nil, format: nil, palette: nil, gamma: nil)
68
+ @app = app
69
+ @name = name || self.class.next_name
70
+
71
+ kwargs = {}
72
+ kwargs[:width] = width if width
73
+ kwargs[:height] = height if height
74
+ kwargs[:file] = file if file
75
+ kwargs[:data] = data if data
76
+ kwargs[:format] = format if format
77
+ kwargs[:palette] = palette if palette
78
+ kwargs[:gamma] = gamma if gamma
79
+
80
+ @app.command(:image, :create, :photo, @name, **kwargs)
81
+ end
82
+
83
+ # Write RGBA pixel data to the image.
84
+ #
85
+ # @param pixel_data [String] binary string, 4 bytes (RGBA) per pixel
86
+ # @param width [Integer] width of the pixel block
87
+ # @param height [Integer] height of the pixel block
88
+ # @param x [Integer] destination X offset
89
+ # @param y [Integer] destination Y offset
90
+ # @param format [:rgba, :argb] pixel format
91
+ # @param composite [:set, :overlay] compositing rule
92
+ # @return [self]
93
+ def put_block(pixel_data, width, height, x: 0, y: 0, format: :rgba, composite: :set)
94
+ opts = { x: x, y: y, format: format, composite: composite }
95
+ @app.interp.photo_put_block(@name, pixel_data, width, height, opts)
96
+ self
97
+ end
98
+
99
+ # Write RGBA pixel data with zoom and subsample.
100
+ #
101
+ # Zoom replicates each pixel (zoom=3 makes each source pixel 3x3).
102
+ # Subsample skips source pixels (subsample=2 takes every other pixel).
103
+ #
104
+ # @param pixel_data [String] binary string, 4 bytes (RGBA) per pixel
105
+ # @param width [Integer] source width in pixels
106
+ # @param height [Integer] source height in pixels
107
+ # @param x [Integer] destination X offset
108
+ # @param y [Integer] destination Y offset
109
+ # @param zoom_x [Integer] horizontal zoom factor
110
+ # @param zoom_y [Integer] vertical zoom factor
111
+ # @param subsample_x [Integer] horizontal subsample factor
112
+ # @param subsample_y [Integer] vertical subsample factor
113
+ # @param format [:rgba, :argb] pixel format
114
+ # @param composite [:set, :overlay] compositing rule
115
+ # @return [self]
116
+ def put_zoomed_block(pixel_data, width, height,
117
+ x: 0, y: 0, zoom_x: 1, zoom_y: 1,
118
+ subsample_x: 1, subsample_y: 1,
119
+ format: :rgba, composite: :set)
120
+ opts = {
121
+ x: x, y: y,
122
+ zoom_x: zoom_x, zoom_y: zoom_y,
123
+ subsample_x: subsample_x, subsample_y: subsample_y,
124
+ format: format, composite: composite
125
+ }
126
+ @app.interp.photo_put_zoomed_block(@name, pixel_data, width, height, opts)
127
+ self
128
+ end
129
+
130
+ # Read pixel data from the image.
131
+ #
132
+ # @param x [Integer] source X offset
133
+ # @param y [Integer] source Y offset
134
+ # @param width [Integer, nil] region width (nil for full image)
135
+ # @param height [Integer, nil] region height (nil for full image)
136
+ # @param unpack [Boolean] if true, return flat array of integers instead of binary string
137
+ # @return [Hash] +{ data: String, width: Integer, height: Integer }+ or
138
+ # +{ pixels: Array<Integer>, width: Integer, height: Integer }+ if unpack is true
139
+ def get_image(x: nil, y: nil, width: nil, height: nil, unpack: false)
140
+ opts = { unpack: unpack }
141
+ opts[:x] = x if x
142
+ opts[:y] = y if y
143
+ opts[:width] = width if width
144
+ opts[:height] = height if height
145
+ @app.interp.photo_get_image(@name, opts)
146
+ end
147
+
148
+ # Read a single pixel.
149
+ #
150
+ # @param x [Integer] X coordinate
151
+ # @param y [Integer] Y coordinate
152
+ # @return [Array<Integer>] [r, g, b, a] values (0-255)
153
+ def get_pixel(x, y)
154
+ @app.interp.photo_get_pixel(@name, x, y)
155
+ end
156
+
157
+ # Get image dimensions.
158
+ #
159
+ # @return [Array<Integer>] [width, height]
160
+ def get_size
161
+ @app.interp.photo_get_size(@name)
162
+ end
163
+
164
+ # Set image dimensions. May crop or add transparent pixels.
165
+ #
166
+ # @param width [Integer] new width
167
+ # @param height [Integer] new height
168
+ # @return [self]
169
+ def set_size(width, height)
170
+ @app.interp.photo_set_size(@name, width, height)
171
+ self
172
+ end
173
+
174
+ # Expand image to at least the given dimensions. Will not shrink.
175
+ #
176
+ # @note Has no effect on photos created with explicit +width:+ / +height:+
177
+ # options. Only works on auto-sized photos (those whose size was set by
178
+ # writing pixel data). This is a Tk limitation.
179
+ #
180
+ # @param width [Integer] minimum width
181
+ # @param height [Integer] minimum height
182
+ # @return [self]
183
+ def expand(width, height)
184
+ @app.interp.photo_expand(@name, width, height)
185
+ self
186
+ end
187
+
188
+ # Clear the image to fully transparent.
189
+ #
190
+ # @return [self]
191
+ def blank
192
+ @app.interp.photo_blank(@name)
193
+ self
194
+ end
195
+
196
+ alias clear blank
197
+
198
+ # Delete this photo image and free its resources.
199
+ #
200
+ # @return [void]
201
+ def delete
202
+ @app.tcl_eval("image delete #{@name}")
203
+ end
204
+
205
+ # Check if this photo image still exists.
206
+ #
207
+ # @return [Boolean]
208
+ def exist?
209
+ @app.tcl_eval("image type #{@name}") == 'photo'
210
+ rescue Teek::TclError
211
+ false
212
+ end
213
+
214
+ # @return [String] the Tcl image name
215
+ def to_s
216
+ @name
217
+ end
218
+
219
+ def inspect
220
+ "#<Teek::Photo #{@name}>"
221
+ end
222
+
223
+ def ==(other)
224
+ other.is_a?(Photo) ? @name == other.name : @name == other.to_s
225
+ end
226
+ alias eql? ==
227
+
228
+ def hash
229
+ @name.hash
230
+ end
231
+ end
232
+ end
data/lib/teek/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Teek
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/teek.rb CHANGED
@@ -4,6 +4,7 @@ require 'tcltklib'
4
4
  require_relative 'teek/version'
5
5
  require_relative 'teek/ractor_support'
6
6
  require_relative 'teek/widget'
7
+ require_relative 'teek/photo'
7
8
 
8
9
  # Ruby interface to Tcl/Tk. Provides a thin wrapper around a Tcl interpreter
9
10
  # with Ruby callbacks, event bindings, and background work support.
@@ -313,7 +314,8 @@ module Teek
313
314
  # @param widget [String] Tk widget path (e.g. ".frame1")
314
315
  # @return [void]
315
316
  # @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/destroy.htm destroy
316
- def destroy(widget)
317
+ def destroy(widget = '.')
318
+ raise ArgumentError, 'widget path cannot be nil' if widget.nil?
317
319
  tcl_eval("destroy #{widget}")
318
320
  end
319
321