teek 0.1.0 → 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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +99 -15
  3. data/Rakefile +201 -2
  4. data/ext/teek/extconf.rb +1 -1
  5. data/ext/teek/tcltkbridge.c +3 -110
  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 +35 -6
  11. data/lib/teek/debugger.rb +37 -32
  12. data/lib/teek/method_coverage_service.rb +265 -0
  13. data/lib/teek/photo.rb +232 -0
  14. data/lib/teek/ractor_support.rb +1 -1
  15. data/lib/teek/version.rb +1 -1
  16. data/lib/teek/widget.rb +104 -0
  17. data/lib/teek.rb +144 -1
  18. data/sample/calculator.rb +16 -21
  19. data/sample/debug_demo.rb +20 -22
  20. data/sample/optcarrot/vendor/optcarrot/apu.rb +856 -0
  21. data/sample/optcarrot/vendor/optcarrot/config.rb +257 -0
  22. data/sample/optcarrot/vendor/optcarrot/cpu.rb +1162 -0
  23. data/sample/optcarrot/vendor/optcarrot/driver.rb +144 -0
  24. data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +14 -0
  25. data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +105 -0
  26. data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +153 -0
  27. data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +14 -0
  28. data/sample/optcarrot/vendor/optcarrot/nes.rb +105 -0
  29. data/sample/optcarrot/vendor/optcarrot/opt.rb +168 -0
  30. data/sample/optcarrot/vendor/optcarrot/pad.rb +92 -0
  31. data/sample/optcarrot/vendor/optcarrot/palette.rb +65 -0
  32. data/sample/optcarrot/vendor/optcarrot/ppu.rb +1468 -0
  33. data/sample/optcarrot/vendor/optcarrot/rom.rb +143 -0
  34. data/sample/optcarrot/vendor/optcarrot.rb +14 -0
  35. data/sample/optcarrot.rb +354 -0
  36. data/sample/paint/assets/bucket.png +0 -0
  37. data/sample/paint/assets/cursor.png +0 -0
  38. data/sample/paint/assets/eraser.png +0 -0
  39. data/sample/paint/assets/pencil.png +0 -0
  40. data/sample/paint/assets/spray.png +0 -0
  41. data/sample/paint/layer.rb +255 -0
  42. data/sample/paint/layer_manager.rb +179 -0
  43. data/sample/paint/paint_demo.rb +837 -0
  44. data/sample/paint/sparse_pixel_buffer.rb +202 -0
  45. data/sample/sdl2_demo.rb +318 -0
  46. data/sample/threading_demo.rb +127 -132
  47. metadata +31 -1
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'sparse_pixel_buffer'
4
+
5
+ # A drawing layer with both pixel (raster) and canvas item (vector) sub-layers.
6
+ #
7
+ # Pixels are stored sparsely for memory efficiency. The Teek::Photo is
8
+ # created lazily when first needed.
9
+ #
10
+ # Canvas items belonging to this layer are tracked so they can be
11
+ # shown/hidden together and properly ordered in the z-stack.
12
+ #
13
+ class Layer
14
+ attr_reader :id, :name, :pixels, :items
15
+ attr_accessor :visible, :opacity
16
+
17
+ # White opaque pixel for background layer
18
+ WHITE_PIXEL = "\xFF\xFF\xFF\xFF".b.freeze
19
+ # Transparent pixel for overlay layers
20
+ TRANSPARENT_PIXEL = "\x00\x00\x00\x00".b.freeze
21
+
22
+ def initialize(app, canvas, width, height, name: "Layer", background: false)
23
+ @id = object_id.to_s(16)
24
+ @app = app
25
+ @canvas = canvas
26
+ @width = width
27
+ @height = height
28
+ @name = name
29
+ @visible = true
30
+ @opacity = 1.0
31
+
32
+ # Pixel sub-layer (sparse storage)
33
+ default = background ? WHITE_PIXEL : TRANSPARENT_PIXEL
34
+ @pixels = SparsePixelBuffer.new(width, height, default: default)
35
+ @background = background
36
+
37
+ # For background layer, we need to fill initially
38
+ if background
39
+ # Don't actually store all pixels - the default handles it
40
+ # Just mark that we need a photo
41
+ @needs_photo = true
42
+ end
43
+
44
+ # Photo image for display (lazily created)
45
+ @photo = nil
46
+ @photo_item = nil
47
+
48
+ # Canvas items belonging to this layer
49
+ @items = []
50
+ end
51
+
52
+ def background?
53
+ @background
54
+ end
55
+
56
+ # Ensure photo image exists and is on canvas
57
+ def ensure_photo!
58
+ return @photo if @photo
59
+
60
+ @photo = Teek::Photo.new(@app, width: @width, height: @height)
61
+ @photo_item = @app.command(@canvas, :create, :image, 0, 0,
62
+ image: @photo.name, anchor: :nw)
63
+
64
+ # For background layer, fill with default color immediately
65
+ if @background
66
+ buffer = @pixels.default_pixel * (@width * @height)
67
+ @photo.put_block(buffer, @width, @height)
68
+ end
69
+
70
+ @photo
71
+ end
72
+
73
+ # Update the photo display from pixel buffer
74
+ def refresh_display
75
+ return unless @visible
76
+
77
+ if @pixels.empty? && !@background
78
+ # Nothing to display, hide photo if it exists
79
+ if @photo_item
80
+ @app.command(@canvas, :itemconfigure, @photo_item, state: :hidden)
81
+ end
82
+ return
83
+ end
84
+
85
+ ensure_photo!
86
+ @app.command(@canvas, :itemconfigure, @photo_item, state: :normal)
87
+
88
+ if @background || @pixels.density > 0.25
89
+ # Dense or background: update entire photo
90
+ buffer = @pixels.materialize
91
+ @photo.put_block(buffer, @width, @height)
92
+ else
93
+ # Sparse: only update bounding box region
94
+ bbox = @pixels.bbox_xywh
95
+ return unless bbox
96
+
97
+ buffer = @pixels.materialize_bbox
98
+ @photo.put_block(buffer, bbox[2], bbox[3], x: bbox[0], y: bbox[1])
99
+ end
100
+ end
101
+
102
+ # Update just a region of the photo (for incremental updates)
103
+ def refresh_region(x, y, width, height)
104
+ return unless @visible
105
+ ensure_photo!
106
+
107
+ buffer = @pixels.materialize(x: x, y: y, width: width, height: height)
108
+ @photo.put_block(buffer, width, height, x: x, y: y)
109
+ end
110
+
111
+ # Pixel operations (delegate to sparse buffer)
112
+ def get_pixel(x, y)
113
+ @pixels.get_pixel(x, y)
114
+ end
115
+
116
+ def set_pixel(x, y, rgba_bytes)
117
+ @pixels.set_pixel(x, y, rgba_bytes)
118
+ end
119
+
120
+ def get_rgba(x, y)
121
+ @pixels.get_rgba(x, y)
122
+ end
123
+
124
+ def set_rgba(x, y, r, g, b, a = 255)
125
+ @pixels.set_rgba(x, y, r, g, b, a)
126
+ end
127
+
128
+ # Canvas item operations
129
+ def add_item(item)
130
+ @items << item
131
+ item
132
+ end
133
+
134
+ def remove_item(item)
135
+ @items.delete(item)
136
+ @app.command(@canvas, :delete, item)
137
+ end
138
+
139
+ def clear_items
140
+ @items.each { |item| @app.command(@canvas, :delete, item) }
141
+ @items.clear
142
+ end
143
+
144
+ # Visibility
145
+ def show
146
+ @visible = true
147
+ if @photo_item
148
+ @app.command(@canvas, :itemconfigure, @photo_item, state: :normal)
149
+ end
150
+ @items.each { |item| @app.command(@canvas, :itemconfigure, item, state: :normal) }
151
+ end
152
+
153
+ def hide
154
+ @visible = false
155
+ if @photo_item
156
+ @app.command(@canvas, :itemconfigure, @photo_item, state: :hidden)
157
+ end
158
+ @items.each { |item| @app.command(@canvas, :itemconfigure, item, state: :hidden) }
159
+ end
160
+
161
+ def toggle_visibility
162
+ @visible ? hide : show
163
+ end
164
+
165
+ # Z-ordering - raise this layer above another
166
+ def raise_above(other_layer)
167
+ if other_layer.photo_item
168
+ @app.command(@canvas, :raise, @photo_item, other_layer.photo_item) if @photo_item
169
+ end
170
+ # Raise all our items above their items
171
+ other_top_item = other_layer.items.last
172
+ if other_top_item
173
+ @items.each { |item| @app.command(@canvas, :raise, item, other_top_item) }
174
+ end
175
+ end
176
+
177
+ # Raise to top of canvas
178
+ def raise_to_top
179
+ @app.command(@canvas, :raise, @photo_item) if @photo_item
180
+ @items.each { |item| @app.command(@canvas, :raise, item) }
181
+ end
182
+
183
+ # Lower to bottom of canvas
184
+ def lower_to_bottom
185
+ @items.reverse_each { |item| @app.command(@canvas, :lower, item) }
186
+ @app.command(@canvas, :lower, @photo_item) if @photo_item
187
+ end
188
+
189
+ # Resize layer to new dimensions
190
+ def resize(new_width, new_height)
191
+ return if new_width == @width && new_height == @height
192
+
193
+ @width = new_width
194
+ @height = new_height
195
+ @pixels.resize(new_width, new_height)
196
+
197
+ if @photo
198
+ @photo.set_size(new_width, new_height)
199
+ refresh_display
200
+ end
201
+ end
202
+
203
+ # Clear everything
204
+ def clear
205
+ @pixels.clear
206
+ clear_items
207
+ if @photo && @background
208
+ # Reset background to default color
209
+ buffer = @pixels.default_pixel * (@width * @height)
210
+ @photo.put_block(buffer, @width, @height)
211
+ elsif @photo
212
+ # Clear to transparent
213
+ if @photo_item
214
+ @app.command(@canvas, :itemconfigure, @photo_item, state: :hidden)
215
+ end
216
+ end
217
+ end
218
+
219
+ # Memory usage
220
+ def memory_usage
221
+ pixels_mem = @pixels.memory_usage
222
+ photo_mem = @photo ? (@width * @height * 4) : 0
223
+ items_mem = @items.size * 100 # Rough estimate per item
224
+ {
225
+ pixels: pixels_mem,
226
+ photo: photo_mem,
227
+ items: items_mem,
228
+ total: pixels_mem + photo_mem + items_mem
229
+ }
230
+ end
231
+
232
+ # For undo system - snapshot current pixel state
233
+ def snapshot_pixels
234
+ @pixels.dup
235
+ end
236
+
237
+ # For undo system - restore pixel state
238
+ def restore_pixels(snapshot)
239
+ @pixels = snapshot.dup
240
+ refresh_display
241
+ end
242
+
243
+ # Destroy the layer
244
+ def destroy
245
+ clear_items
246
+ @app.command(@canvas, :delete, @photo_item) if @photo_item
247
+ @photo&.delete
248
+ @photo = nil
249
+ @photo_item = nil
250
+ end
251
+
252
+ protected
253
+
254
+ attr_reader :photo_item
255
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'layer'
4
+
5
+ # Manages a stack of layers for the paint application.
6
+ #
7
+ # Handles layer creation, ordering, and the concept of an "active" layer
8
+ # that receives drawing operations.
9
+ #
10
+ class LayerManager
11
+ attr_reader :layers, :width, :height
12
+
13
+ def initialize(app, canvas, width, height)
14
+ @app = app
15
+ @canvas = canvas
16
+ @width = width
17
+ @height = height
18
+ @layers = []
19
+ @active_index = 0
20
+
21
+ # Create default background layer
22
+ add_layer(name: "Background", background: true)
23
+ end
24
+
25
+ def active_layer
26
+ @active_index ? @layers[@active_index] : nil
27
+ end
28
+
29
+ def active_layer=(layer)
30
+ idx = @layers.index(layer)
31
+ @active_index = idx if idx
32
+ end
33
+
34
+ def active_index
35
+ @active_index
36
+ end
37
+
38
+ def active_index=(index)
39
+ @active_index = index if index >= 0 && index < @layers.size
40
+ end
41
+
42
+ # Add a new layer at the top (or at specific index)
43
+ def add_layer(name: nil, background: false, index: nil)
44
+ name ||= "Layer #{@layers.size}"
45
+ layer = Layer.new(@app, @canvas, @width, @height, name: name, background: background)
46
+
47
+ if index
48
+ @layers.insert(index, layer)
49
+ # Adjust active index if needed
50
+ @active_index += 1 if @active_index && index <= @active_index
51
+ else
52
+ @layers << layer
53
+ end
54
+
55
+ # New layer becomes active
56
+ @active_index = @layers.index(layer)
57
+
58
+ reorder_canvas_items
59
+ layer
60
+ end
61
+
62
+ # Remove a layer
63
+ def remove_layer(layer_or_index)
64
+ layer = layer_or_index.is_a?(Layer) ? layer_or_index : @layers[layer_or_index]
65
+ return nil unless layer
66
+ return nil if layer.background? && @layers.size == 1 # Can't remove only background
67
+
68
+ idx = @layers.index(layer)
69
+ @layers.delete(layer)
70
+ layer.destroy
71
+
72
+ # Adjust active index
73
+ if @active_index
74
+ if @active_index == idx
75
+ @active_index = [idx, @layers.size - 1].min
76
+ elsif @active_index > idx
77
+ @active_index -= 1
78
+ end
79
+ end
80
+
81
+ layer
82
+ end
83
+
84
+ # Move layer up (towards top/front)
85
+ def move_up(layer_or_index)
86
+ idx = layer_or_index.is_a?(Layer) ? @layers.index(layer_or_index) : layer_or_index
87
+ return false if idx.nil? || idx >= @layers.size - 1
88
+
89
+ @layers[idx], @layers[idx + 1] = @layers[idx + 1], @layers[idx]
90
+ @active_index = idx + 1 if @active_index == idx
91
+ reorder_canvas_items
92
+ true
93
+ end
94
+
95
+ # Move layer down (towards bottom/back)
96
+ def move_down(layer_or_index)
97
+ idx = layer_or_index.is_a?(Layer) ? @layers.index(layer_or_index) : layer_or_index
98
+ return false if idx.nil? || idx <= 0
99
+
100
+ @layers[idx], @layers[idx - 1] = @layers[idx - 1], @layers[idx]
101
+ @active_index = idx - 1 if @active_index == idx
102
+ reorder_canvas_items
103
+ true
104
+ end
105
+
106
+ # Reorder all canvas items to match layer stack
107
+ # Bottom of @layers array = back of canvas (lowest z-order)
108
+ def reorder_canvas_items
109
+ @layers.each_with_index do |layer, _idx|
110
+ layer.raise_to_top
111
+ end
112
+ end
113
+
114
+ # Resize all layers to new dimensions
115
+ def resize(new_width, new_height)
116
+ return if new_width == @width && new_height == @height
117
+
118
+ @width = new_width
119
+ @height = new_height
120
+ @layers.each { |l| l.resize(new_width, new_height) }
121
+ end
122
+
123
+ # Find layer by name or id
124
+ def find(name_or_id)
125
+ @layers.find { |l| l.name == name_or_id || l.id == name_or_id }
126
+ end
127
+
128
+ # Refresh all layer displays
129
+ def refresh_all
130
+ @layers.each(&:refresh_display)
131
+ end
132
+
133
+ # Clear all layers
134
+ def clear_all
135
+ @layers.each(&:clear)
136
+ end
137
+
138
+ # Merge visible layers down to background (flatten)
139
+ def flatten
140
+ return if @layers.size <= 1
141
+
142
+ bg = @layers.first
143
+ bg_pixels = bg.pixels
144
+
145
+ # Composite each layer on top (simple overwrite for now, no alpha blending)
146
+ @layers[1..].each do |layer|
147
+ next unless layer.visible
148
+
149
+ layer.pixels.each_pixel do |x, y, rgba|
150
+ # Simple overwrite (could add alpha blending later)
151
+ a = rgba.unpack1('@3C') # Get alpha byte
152
+ bg_pixels.set_pixel(x, y, rgba) if a > 0
153
+ end
154
+ end
155
+
156
+ # Remove all layers except background
157
+ @layers[1..].each(&:destroy)
158
+ @layers = [@layers.first]
159
+ @active_index = 0
160
+
161
+ bg.refresh_display
162
+ end
163
+
164
+ # Memory usage across all layers
165
+ def memory_usage
166
+ @layers.sum { |l| l.memory_usage[:total] }
167
+ end
168
+
169
+ # Debug info
170
+ def to_s
171
+ lines = ["LayerManager: #{@layers.size} layers, active=#{@active_index}"]
172
+ @layers.each_with_index do |layer, idx|
173
+ marker = idx == @active_index ? '>' : ' '
174
+ vis = layer.visible ? 'V' : 'H'
175
+ lines << " #{marker}[#{idx}] #{vis} #{layer.name} (#{layer.pixels.pixel_count} px)"
176
+ end
177
+ lines.join("\n")
178
+ end
179
+ end