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.
- checksums.yaml +4 -4
- data/README.md +99 -15
- data/Rakefile +201 -2
- data/ext/teek/extconf.rb +1 -1
- data/ext/teek/tcltkbridge.c +3 -110
- data/ext/teek/tcltkbridge.h +3 -0
- data/ext/teek/tkeventsource.c +195 -0
- data/ext/teek/tkphoto.c +169 -5
- data/ext/teek/tkwin.c +84 -0
- data/lib/teek/background_ractor4x.rb +35 -6
- data/lib/teek/debugger.rb +37 -32
- data/lib/teek/method_coverage_service.rb +265 -0
- data/lib/teek/photo.rb +232 -0
- data/lib/teek/ractor_support.rb +1 -1
- data/lib/teek/version.rb +1 -1
- data/lib/teek/widget.rb +104 -0
- data/lib/teek.rb +144 -1
- data/sample/calculator.rb +16 -21
- data/sample/debug_demo.rb +20 -22
- data/sample/optcarrot/vendor/optcarrot/apu.rb +856 -0
- data/sample/optcarrot/vendor/optcarrot/config.rb +257 -0
- data/sample/optcarrot/vendor/optcarrot/cpu.rb +1162 -0
- data/sample/optcarrot/vendor/optcarrot/driver.rb +144 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +14 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +105 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +153 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +14 -0
- data/sample/optcarrot/vendor/optcarrot/nes.rb +105 -0
- data/sample/optcarrot/vendor/optcarrot/opt.rb +168 -0
- data/sample/optcarrot/vendor/optcarrot/pad.rb +92 -0
- data/sample/optcarrot/vendor/optcarrot/palette.rb +65 -0
- data/sample/optcarrot/vendor/optcarrot/ppu.rb +1468 -0
- data/sample/optcarrot/vendor/optcarrot/rom.rb +143 -0
- data/sample/optcarrot/vendor/optcarrot.rb +14 -0
- data/sample/optcarrot.rb +354 -0
- data/sample/paint/assets/bucket.png +0 -0
- data/sample/paint/assets/cursor.png +0 -0
- data/sample/paint/assets/eraser.png +0 -0
- data/sample/paint/assets/pencil.png +0 -0
- data/sample/paint/assets/spray.png +0 -0
- data/sample/paint/layer.rb +255 -0
- data/sample/paint/layer_manager.rb +179 -0
- data/sample/paint/paint_demo.rb +837 -0
- data/sample/paint/sparse_pixel_buffer.rb +202 -0
- data/sample/sdl2_demo.rb +318 -0
- data/sample/threading_demo.rb +127 -132
- 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
|