-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy path_COMMON.BM
More file actions
397 lines (339 loc) · 13.9 KB
/
Copy path_COMMON.BM
File metadata and controls
397 lines (339 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
''
' DRAW - COMMON.BM
' =============================================================================
' Common subs and functions
'
' @author Rick Christy <grymmjack@gmail.com>
'
$INCLUDEONCE
''
' Apply paint opacity to a color value.
' Modulates the color's alpha channel by PAINT_OPACITY% (10-100).
' Eraser colors (alpha=0) pass through unchanged.
' @param col _UNSIGNED LONG Source color to apply opacity to
' @return _UNSIGNED LONG - Color with adjusted alpha channel
'
FUNCTION PAINT_apply_opacity~& (col AS _UNSIGNED LONG)
IF PAINT_OPACITY% >= 100 OR _ALPHA32(col~&) = 0 THEN
PAINT_apply_opacity~& = col~&
ELSE
DIM opA AS INTEGER
opA% = INT(_ALPHA32(col~&) * PAINT_OPACITY% / 100)
IF opA% < 0 THEN opA% = 0
IF opA% > 255 THEN opA% = 255
PAINT_apply_opacity~& = _RGBA32(_RED32(col~&), _GREEN32(col~&), _BLUE32(col~&), opA%)
END IF
END FUNCTION
''
' Begin a stroke for opacity compositing.
' Saves a backup of the current layer so we can blend at commit time.
' All drawing during the stroke happens at full opacity on the real layer.
' Call STROKE_commit when the stroke/operation finishes.
'
SUB STROKE_begin ()
IF PAINT_OPACITY% >= 100 THEN EXIT SUB ' Full opacity needs no buffering
IF STROKE_ACTIVE% THEN EXIT SUB ' Already in a stroke
DIM layerImg AS LONG
layerImg& = LAYERS(CURRENT_LAYER%).imgHandle&
IF layerImg& >= -1 THEN EXIT SUB ' Invalid image handle
' Free any stale backup
IF STROKE_BACKUP& < -1 THEN SAFE_FREEIMAGE STROKE_BACKUP&
' Save current layer state
STROKE_BACKUP& = _COPYIMAGE(layerImg&, 32)
STROKE_ACTIVE% = TRUE
END SUB
''
' Commit (end) a stroke, applying paint opacity.
' For each pixel that changed during the stroke, lerps between the backup
' (original) and current (full-opacity stroke) by PAINT_OPACITY%.
' This ensures overlapping brush dabs within a stroke don't compound opacity.
'
SUB STROKE_commit ()
IF NOT STROKE_ACTIVE% THEN EXIT SUB
STROKE_ACTIVE% = FALSE
' If opacity was changed to 100% mid-stroke, just clean up
IF PAINT_OPACITY% >= 100 THEN
IF STROKE_BACKUP& < -1 THEN SAFE_FREEIMAGE STROKE_BACKUP&
STROKE_BACKUP& = 0
EXIT SUB
END IF
DIM layerImg AS LONG
layerImg& = LAYERS(CURRENT_LAYER%).imgHandle&
IF layerImg& >= -1 OR STROKE_BACKUP& >= -1 THEN
IF STROKE_BACKUP& < -1 THEN SAFE_FREEIMAGE STROKE_BACKUP&
STROKE_BACKUP& = 0
EXIT SUB
END IF
' Layer pixels are about to be opacity-blended — force a full re-render so the
' result is visible. INVALIDATE_scene sets SCENE_DIRTY% + GUI_NEEDS_REDRAW%,
' ensuring the idle-detection block keeps FRAME_IDLE% = FALSE this frame.
INVALIDATE_scene
' Use _MEM for fast pixel-level blending
DIM mLayer AS _MEM, mBackup AS _MEM
mLayer = _MEMIMAGE(layerImg&)
mBackup = _MEMIMAGE(STROKE_BACKUP&)
DIM opPct AS INTEGER
opPct% = PAINT_OPACITY%
DIM pxOffset AS _OFFSET
DIM curPixel AS _UNSIGNED LONG
DIM bakPixel AS _UNSIGNED LONG
DIM cr AS INTEGER, cg AS INTEGER, cb AS INTEGER, ca AS INTEGER
DIM br AS INTEGER, bg AS INTEGER, bb AS INTEGER, ba AS INTEGER
DIM nr AS INTEGER, ng AS INTEGER, nb AS INTEGER, na AS INTEGER
FOR pxOffset = 0 TO mLayer.SIZE - 4 STEP 4
_MEMGET mLayer, mLayer.OFFSET + pxOffset, curPixel~&
_MEMGET mBackup, mBackup.OFFSET + pxOffset, bakPixel~&
IF curPixel~& <> bakPixel~& THEN
' Pixel was changed by the stroke — lerp toward original by (1 - opacity)
cr% = _RED32(curPixel~&) : cg% = _GREEN32(curPixel~&)
cb% = _BLUE32(curPixel~&) : ca% = _ALPHA32(curPixel~&)
br% = _RED32(bakPixel~&) : bg% = _GREEN32(bakPixel~&)
bb% = _BLUE32(bakPixel~&) : ba% = _ALPHA32(bakPixel~&)
nr% = br% + (cr% - br%) * opPct% \ 100
ng% = bg% + (cg% - bg%) * opPct% \ 100
nb% = bb% + (cb% - bb%) * opPct% \ 100
na% = ba% + (ca% - ba%) * opPct% \ 100
_MEMPUT mLayer, mLayer.OFFSET + pxOffset, _RGBA32(nr%, ng%, nb%, na%) AS _UNSIGNED LONG
END IF
NEXT
_MEMFREE mLayer
_MEMFREE mBackup
' Clean up backup
IF STROKE_BACKUP& < -1 THEN SAFE_FREEIMAGE STROKE_BACKUP&
STROKE_BACKUP& = 0
END SUB
''
' Update window title bar to reflect current state
' Format: DRAW vX.X.X - filename *
'
SUB TITLE_update ()
DIM t$
DIM fn$
t$ = "DRAW v" + APP_VERSION$
' Determine active filename (prefer .draw project, fall back to image file)
IF CURRENT_DRW_FILENAME$ <> "" THEN
fn$ = CURRENT_DRW_FILENAME$
ELSEIF CURRENT_FILENAME$ <> "" THEN
fn$ = CURRENT_FILENAME$
END IF
' Extract just the filename (no path)
IF fn$ <> "" THEN
DIM i AS INTEGER
DIM baseName$
baseName$ = fn$
FOR i% = LEN(fn$) TO 1 STEP -1
IF MID$(fn$, i%, 1) = "/" OR MID$(fn$, i%, 1) = "\" THEN
baseName$ = MID$(fn$, i% + 1)
EXIT FOR
END IF
NEXT i%
t$ = t$ + " - " + baseName$
END IF
' Dirty indicator
IF CANVAS_DIRTY% THEN t$ = t$ + " *"
' Character Mode hint
IF CHARMAP.useChars% THEN t$ = t$ + " (CHAR MODE)"
' Multi-instance tag: show [N] on secondary instances, and on the primary too once
' a peer is running, so it's clear which window is which. (INST.liveCount is refreshed
' by the throttled INSTANCE_tick heartbeat — no filesystem scan here.)
IF INST.id% > 1 OR INST.liveCount% > 1 THEN t$ = t$ + " [" + _TRIM$(STR$(INST.id%)) + "]"
_TITLE t$
' Update tracking state
TITLE_PREV_DIRTY% = CANVAS_DIRTY%
TITLE_PREV_FILENAME$ = fn$
END SUB
''
' Check if title needs updating and call TITLE_update if so
'
SUB TITLE_check ()
STATIC prevCharMode AS INTEGER
DIM fn$
IF CURRENT_DRW_FILENAME$ <> "" THEN
fn$ = CURRENT_DRW_FILENAME$
ELSEIF CURRENT_FILENAME$ <> "" THEN
fn$ = CURRENT_FILENAME$
END IF
IF CANVAS_DIRTY% <> TITLE_PREV_DIRTY% OR fn$ <> TITLE_PREV_FILENAME$ OR CHARMAP.useChars% <> prevCharMode% THEN
TITLE_update
prevCharMode% = CHARMAP.useChars%
END IF
END SUB
''
' Returns an int as a trimmed string
' @param v INTEGER64 Integer value to convert to string
' @return STRING - Trimmed string representation of the integer
'
FUNCTION ns$ (v&&)
ns$ = _TRIM$(STR$(v&&))
END FUNCTION
''
' Snap a line endpoint to the nearest angle increment (e.g., 45 degrees)
' Used when drawing lines/polygons with Ctrl+Shift held for precise angles
'
' @param start_x INTEGER Starting X coordinate
' @param start_y INTEGER Starting Y coordinate
' @param end_x INTEGER Current end X coordinate (will be modified)
' @param end_y INTEGER Current end Y coordinate (will be modified)
' @param snap_degrees INTEGER Angle snap increment (15, 30, 45, 90, etc.)
'
SUB SNAP_to_angle (start_x AS INTEGER, start_y AS INTEGER, end_x AS INTEGER, end_y AS INTEGER, snap_degrees AS INTEGER)
DIM dx AS SINGLE, dy AS SINGLE, angle AS SINGLE, distance AS SINGLE
dx! = end_x - start_x
dy! = end_y - start_y
' If start and end are the same point, nothing to snap
IF dx! = 0 AND dy! = 0 THEN EXIT SUB
IF CFG.ANGLE_SNAP_PIXEL_ART% THEN
' === Pixel Art Angle Snap ===
' Snap to nearest angle from integer slope ratios (0:1 through 5:1)
' which produce clean aliasing-free lines in pixel art.
' Uses the ratio directly to compute endpoints (no trig = pixel perfect).
'
' 22 pixel-art angles in full circle (11 per half, mirrored):
' 0, 11.31, 14.04, 18.43, 26.57, 45, 63.43, 71.57, 75.96, 78.69, 90
' and their negatives/mirrors
' Lookup table: integer slope ratios for first quadrant (0..90 degrees)
' Each entry is (run, rise) where angle = atan2(rise, run)
' Stored as pairs: run(i), rise(i)
CONST PA_NUM_ANGLES = 11
DIM pa_run(0 TO 10) AS INTEGER
DIM pa_rise(0 TO 10) AS INTEGER
pa_run(0) = 1 : pa_rise(0) = 0 ' 0.00 deg
pa_run(1) = 5 : pa_rise(1) = 1 ' 11.31 deg
pa_run(2) = 4 : pa_rise(2) = 1 ' 14.04 deg
pa_run(3) = 3 : pa_rise(3) = 1 ' 18.43 deg
pa_run(4) = 2 : pa_rise(4) = 1 ' 26.57 deg
pa_run(5) = 1 : pa_rise(5) = 1 ' 45.00 deg
pa_run(6) = 1 : pa_rise(6) = 2 ' 63.43 deg
pa_run(7) = 1 : pa_rise(7) = 3 ' 71.57 deg
pa_run(8) = 1 : pa_rise(8) = 4 ' 75.96 deg
pa_run(9) = 1 : pa_rise(9) = 5 ' 78.69 deg
pa_run(10) = 0 : pa_rise(10) = 1 ' 90.00 deg
' Calculate angle of the line
angle! = _ATAN2(dy!, dx!)
' Normalize to 0..2*PI
IF angle! < 0 THEN angle! = angle! + 2 * _PI
' Determine which quadrant and mirror into first quadrant (0..PI/2)
DIM qa AS SINGLE
DIM quadrant AS INTEGER
IF angle! <= _PI / 2 THEN
qa! = angle!
quadrant% = 0
ELSEIF angle! <= _PI THEN
qa! = _PI - angle!
quadrant% = 1
ELSEIF angle! <= 3 * _PI / 2 THEN
qa! = angle! - _PI
quadrant% = 2
ELSE
qa! = 2 * _PI - angle!
quadrant% = 3
END IF
' Find closest pixel-art angle in first quadrant
DIM bestIdx AS INTEGER, bestDiff AS SINGLE
DIM pa_angle AS SINGLE, diff AS SINGLE
DIM i AS INTEGER
bestIdx% = 0
bestDiff! = 999!
FOR i% = 0 TO PA_NUM_ANGLES - 1
pa_angle! = _ATAN2(CSNG(pa_rise(i%)), CSNG(pa_run(i%)))
diff! = ABS(qa! - pa_angle!)
IF diff! < bestDiff! THEN
bestDiff! = diff!
bestIdx% = i%
END IF
NEXT i%
' Get the integer slope ratio
DIM bestRun AS INTEGER, bestRise AS INTEGER
bestRun% = pa_run(bestIdx%)
bestRise% = pa_rise(bestIdx%)
' Apply quadrant mirroring to get signed run/rise
DIM signedRun AS INTEGER, signedRise AS INTEGER
SELECT CASE quadrant%
CASE 0 : signedRun% = bestRun% : signedRise% = bestRise% ' Q1 : +x, +y
CASE 1 : signedRun% = -bestRun% : signedRise% = bestRise% ' Q2 : -x, +y
CASE 2 : signedRun% = -bestRun% : signedRise% = -bestRise% ' Q3 : -x, -y
CASE 3 : signedRun% = bestRun% : signedRise% = -bestRise% ' Q4 : +x, -y
END SELECT
' Project endpoint using integer ratio (pixel perfect)
' Find the multiplier that gets closest to the original distance
distance! = SQR(dx! * dx! + dy! * dy!)
DIM stepLen AS SINGLE
stepLen! = SQR(CSNG(bestRun% * bestRun% + bestRise% * bestRise%))
IF stepLen! < 0.001! THEN stepLen! = 1! ' safety for 0,0 (shouldn't happen)
DIM multiplier AS INTEGER
multiplier% = INT(distance! / stepLen! + 0.5!)
IF multiplier% < 1 THEN multiplier% = 1
end_x = start_x + signedRun% * multiplier%
end_y = start_y + signedRise% * multiplier%
ELSE
' === Degree-based Angle Snap (original behavior) ===
DIM snapped_angle AS SINGLE, snap_rad AS SINGLE
' Calculate angle in degrees (-180 to 180)
angle! = _ATAN2(dy!, dx!) * 180 / _PI
' Calculate distance from start to end
distance! = SQR(dx! * dx! + dy! * dy!)
' Snap angle to nearest increment
snapped_angle! = INT(angle! / snap_degrees + 0.5) * snap_degrees
' Convert back to radians
snap_rad! = snapped_angle! * _PI / 180
' Calculate new endpoint at snapped angle with same distance
end_x = start_x + INT(COS(snap_rad!) * distance! + 0.5)
end_y = start_y + INT(SIN(snap_rad!) * distance! + 0.5)
END IF
END SUB
'' ---------------------------------------------------------------------------
' INVALIDATE helpers
' ---------------------------------------------------------------------------
' Mark scene for full repaint (no pixel or layer changes — UI event only).
' Equivalent to the three-line sequence that appears 69+ times in the codebase:
' SCENE_DIRTY% = TRUE : GUI_NEEDS_REDRAW% = TRUE : FRAME_IDLE% = FALSE
'
SUB INVALIDATE_scene ()
SCENE_DIRTY% = TRUE
GUI_NEEDS_REDRAW% = TRUE
FRAME_IDLE% = FALSE
END SUB
'' ---------------------------------------------------------------------------
' Mark scene dirty and track a canvas pixel change (triggers unsaved indicator).
' Equivalent to adding CANVAS_DIRTY% = TRUE before INVALIDATE_scene.
'
SUB INVALIDATE_canvas ()
CANVAS_DIRTY% = TRUE
SCENE_DIRTY% = TRUE
GUI_NEEDS_REDRAW% = TRUE
FRAME_IDLE% = FALSE
END SUB
'' ---------------------------------------------------------------------------
' Invalidate blend composite cache (layer structure or visibility changed),
' mark canvas dirty, and request a full repaint.
' Equivalent to: BLEND_invalidate_cache + INVALIDATE_canvas.
'
SUB INVALIDATE_layers ()
BLEND_invalidate_cache
CANVAS_DIRTY% = TRUE
SCENE_DIRTY% = TRUE
GUI_NEEDS_REDRAW% = TRUE
FRAME_IDLE% = FALSE
END SUB
''
' Expand the per-frame composite dirty rect to include a canvas region.
' RENDER_layers uses this to restrict blend compositing to the changed area.
' @param x1 INTEGER Left edge of the dirty region
' @param y1 INTEGER Top edge of the dirty region
' @param x2 INTEGER Right edge of the dirty region
' @param y2 INTEGER Bottom edge of the dirty region
'
SUB COMP_mark_dirty (x1 AS INTEGER, y1 AS INTEGER, x2 AS INTEGER, y2 AS INTEGER)
IF NOT COMP_DIRTY_VALID% THEN
COMP_DIRTY_X1% = x1%
COMP_DIRTY_Y1% = y1%
COMP_DIRTY_X2% = x2%
COMP_DIRTY_Y2% = y2%
COMP_DIRTY_VALID% = TRUE
ELSE
IF x1% < COMP_DIRTY_X1% THEN COMP_DIRTY_X1% = x1%
IF y1% < COMP_DIRTY_Y1% THEN COMP_DIRTY_Y1% = y1%
IF x2% > COMP_DIRTY_X2% THEN COMP_DIRTY_X2% = x2%
IF y2% > COMP_DIRTY_Y2% THEN COMP_DIRTY_Y2% = y2%
END IF
END SUB