ifmapper 0.5

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 (87) hide show
  1. data/HISTORY.txt +2 -0
  2. data/IFMapper.gemspec +21 -0
  3. data/IFMapper.rb +27 -0
  4. data/icons/copy.png +0 -0
  5. data/icons/cut.png +0 -0
  6. data/icons/filenew.png +0 -0
  7. data/icons/fileopen.png +0 -0
  8. data/icons/filesave.png +0 -0
  9. data/icons/filesaveas.png +0 -0
  10. data/icons/help.png +0 -0
  11. data/icons/kill.png +0 -0
  12. data/icons/nextpage.png +0 -0
  13. data/icons/paste.png +0 -0
  14. data/icons/prevpage.png +0 -0
  15. data/icons/printicon.png +0 -0
  16. data/icons/redo.png +0 -0
  17. data/icons/saveas.png +0 -0
  18. data/icons/undo.png +0 -0
  19. data/icons/winapp.png +0 -0
  20. data/icons/zoom.png +0 -0
  21. data/lib/IFMapper/Connection.rb +63 -0
  22. data/lib/IFMapper/FXAboutDialogBox.rb +32 -0
  23. data/lib/IFMapper/FXConnection.rb +283 -0
  24. data/lib/IFMapper/FXConnectionDialogBox.rb +126 -0
  25. data/lib/IFMapper/FXMap.rb +1614 -0
  26. data/lib/IFMapper/FXMapDialogBox.rb +51 -0
  27. data/lib/IFMapper/FXMapFileDialog.rb +29 -0
  28. data/lib/IFMapper/FXMapperSettings.rb +45 -0
  29. data/lib/IFMapper/FXMapperWindow.rb +1051 -0
  30. data/lib/IFMapper/FXPage.rb +24 -0
  31. data/lib/IFMapper/FXPageDialogBox.rb +38 -0
  32. data/lib/IFMapper/FXRoom.rb +218 -0
  33. data/lib/IFMapper/FXRoomDialogBox.rb +119 -0
  34. data/lib/IFMapper/FXSearchDialogBox.rb +51 -0
  35. data/lib/IFMapper/FXSpline.rb +54 -0
  36. data/lib/IFMapper/FXWarningBox.rb +45 -0
  37. data/lib/IFMapper/IFMReader.rb +613 -0
  38. data/lib/IFMapper/Map.rb +110 -0
  39. data/lib/IFMapper/PDFMapExporter.rb +315 -0
  40. data/lib/IFMapper/Page.rb +158 -0
  41. data/lib/IFMapper/Room.rb +104 -0
  42. data/maps/Bureaucracy.ifm +75 -0
  43. data/maps/Hollywood_Hijinx.ifm +149 -0
  44. data/maps/Jigsaw.ifm +806 -0
  45. data/maps/LGOP.ifm +705 -0
  46. data/maps/Mercy.ifm +76 -0
  47. data/maps/Planetfall.ifm +186 -0
  48. data/maps/Plundered_Hearts.ifm +251 -0
  49. data/maps/Ralph.ifm +50 -0
  50. data/maps/Robots_of_Dawn.ifm +224 -0
  51. data/maps/Seastalker.ifm +149 -0
  52. data/maps/Sherlock.ifm +209 -0
  53. data/maps/SoFar.ifm +72 -0
  54. data/maps/Starcross.ifm +170 -0
  55. data/maps/Suspended.ifm +82 -0
  56. data/maps/Wishbringer.ifm +277 -0
  57. data/maps/Wishbringer2.ifm +246 -0
  58. data/maps/Zork1.ifm +410 -0
  59. data/maps/Zork2.ifm +150 -0
  60. data/maps/Zork3.ifm +136 -0
  61. data/maps/Zork_Zero.ifm +557 -0
  62. data/maps/anchor.ifm +645 -0
  63. data/maps/atrox.ifm +134 -0
  64. data/maps/awaken.ifm +116 -0
  65. data/maps/babel.ifm +279 -0
  66. data/maps/bse.ifm +150 -0
  67. data/maps/change.ifm +128 -0
  68. data/maps/curses.ifm +307 -0
  69. data/maps/curves.ifm +529 -0
  70. data/maps/edifice.ifm +158 -0
  71. data/maps/frozen.ifm +126 -0
  72. data/maps/glow.ifm +101 -0
  73. data/maps/library.ifm +93 -0
  74. data/maps/mindelec.ifm +89 -0
  75. data/maps/minster.ifm +234 -0
  76. data/maps/muse.ifm +154 -0
  77. data/maps/paperchase.ifm +110 -0
  78. data/maps/space_st.ifm +104 -0
  79. data/maps/stationfall.ifm +320 -0
  80. data/maps/theatre.ifm +182 -0
  81. data/maps/toonesia.ifm +54 -0
  82. data/maps/tortoise.ifm +72 -0
  83. data/maps/vgame.ifm +219 -0
  84. data/maps/weather.ifm +98 -0
  85. data/maps/windhall.ifm +154 -0
  86. data/maps/zebulon.ifm +68 -0
  87. metadata +144 -0
@@ -0,0 +1,126 @@
1
+
2
+ #
3
+ # Class used to display a connection dialog box
4
+ #
5
+ class FXConnectionDialogBox < FXDialogBox
6
+
7
+ TYPE_TEXT = [
8
+ 'Free',
9
+ 'Locked',
10
+ 'Special',
11
+ ]
12
+
13
+ DIR_TEXT = [
14
+ 'Both',
15
+ 'A to B',
16
+ 'B to A',
17
+ ]
18
+
19
+ EXIT_TEXT = [
20
+ 'None',
21
+ 'Up',
22
+ 'Down',
23
+ 'In',
24
+ 'Out',
25
+ ]
26
+
27
+ attr_writer :map
28
+
29
+ def copy_to()
30
+ @conn.dir = @dir.currentNo
31
+ @conn.type = @type.currentNo
32
+ @conn.exitAtext = @exitA.currentNo
33
+ @conn.exitBtext = @exitB.currentNo
34
+
35
+ @map.draw
36
+ end
37
+
38
+
39
+ def copy_from(conn)
40
+ title = conn.to_s
41
+ self.title = title
42
+
43
+ @dir.currentNo = conn.dir
44
+ @type.currentNo = conn.type || 0
45
+ @exitA.currentNo = conn.exitAtext
46
+ @exitB.currentNo = conn.exitBtext
47
+ @conn = conn
48
+
49
+ if @map.navigation
50
+ @dir.disable
51
+ @type.disable
52
+ @exitA.disable
53
+ @exitB.disable
54
+ else
55
+ @dir.enable
56
+ @type.enable
57
+ @exitA.enable
58
+ @exitB.enable
59
+ end
60
+ end
61
+
62
+ def initialize(map, conn, event = nil)
63
+ pos = [40, 40]
64
+ if event
65
+ pos = [ event.last_x, event.last_y ]
66
+ end
67
+ maxW = map.window.width - 390
68
+ maxH = map.window.height - 300
69
+ pos[0] = maxW if pos[0] > maxW
70
+ pos[1] = maxH if pos[1] > maxH
71
+
72
+ decor = DECOR_TITLE|DECOR_BORDER|DECOR_CLOSE
73
+ super( map.window, '', decor, pos[0], pos[1], 0, 0 )
74
+ mainFrame = FXVerticalFrame.new(self,
75
+ FRAME_SUNKEN|FRAME_THICK|
76
+ LAYOUT_FILL_X|LAYOUT_FILL_Y)
77
+
78
+ frame = FXHorizontalFrame.new(mainFrame, LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
79
+
80
+ FXLabel.new(frame, "Connection Type: ", nil, 0, LAYOUT_FILL_X)
81
+ pane = FXPopup.new(self)
82
+ TYPE_TEXT.each { |t|
83
+ FXOption.new(pane, t, nil, nil, 0, JUSTIFY_HZ_APART|ICON_AFTER_TEXT)
84
+ }
85
+ @type = FXOptionMenu.new(frame, pane, FRAME_RAISED|FRAME_THICK|
86
+ JUSTIFY_HZ_APART|ICON_AFTER_TEXT|
87
+ LAYOUT_CENTER_X|LAYOUT_CENTER_Y)
88
+
89
+
90
+ FXLabel.new(frame, "Direction: ", nil, 0, LAYOUT_FILL_X)
91
+ pane = FXPopup.new(self)
92
+ DIR_TEXT.each { |t|
93
+ FXOption.new(pane, t, nil, nil, 0, JUSTIFY_HZ_APART|ICON_AFTER_TEXT)
94
+ }
95
+ @dir = FXOptionMenu.new(frame, pane, FRAME_RAISED|FRAME_THICK|
96
+ JUSTIFY_HZ_APART|ICON_AFTER_TEXT|
97
+ LAYOUT_CENTER_X|LAYOUT_CENTER_Y)
98
+
99
+ frame = FXHorizontalFrame.new(mainFrame, LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
100
+ FXLabel.new(frame, "Exit A Text: ", nil, 0, LAYOUT_FILL_X)
101
+ pane = FXPopup.new(self)
102
+ EXIT_TEXT.each { |t|
103
+ FXOption.new(pane, t, nil, nil, 0, JUSTIFY_HZ_APART|ICON_AFTER_TEXT)
104
+ }
105
+ @exitA = FXOptionMenu.new(frame, pane, FRAME_RAISED|FRAME_THICK|
106
+ JUSTIFY_HZ_APART|ICON_AFTER_TEXT|
107
+ LAYOUT_CENTER_X|LAYOUT_CENTER_Y)
108
+ FXLabel.new(frame, "Exit B Text: ", nil, 0, LAYOUT_FILL_X)
109
+ pane = FXPopup.new(self)
110
+ EXIT_TEXT.each { |t|
111
+ FXOption.new(pane, t, nil, nil, 0, JUSTIFY_HZ_APART|ICON_AFTER_TEXT)
112
+ }
113
+ @exitB = FXOptionMenu.new(frame, pane, FRAME_RAISED|FRAME_THICK|
114
+ JUSTIFY_HZ_APART|ICON_AFTER_TEXT|
115
+ LAYOUT_CENTER_X|LAYOUT_CENTER_Y)
116
+
117
+ @dir.connect(SEL_COMMAND) { copy_to() }
118
+ @type.connect(SEL_COMMAND) { copy_to()}
119
+ @exitA.connect(SEL_COMMAND) { copy_to() }
120
+ @exitB.connect(SEL_COMMAND) { copy_to() }
121
+ @map = map
122
+
123
+ # We need to create the dialog box first, so we can use select text.
124
+ create
125
+ end
126
+ end
@@ -0,0 +1,1614 @@
1
+ #!/usr/bin/env ruby
2
+
3
+
4
+ require 'IFMapper/Map'
5
+ require 'IFMapper/FXPage'
6
+ require 'IFMapper/FXMapDialogBox'
7
+ require 'IFMapper/FXPageDialogBox'
8
+
9
+
10
+ class FXMap < Map
11
+ FILE_FORMAT_VERSION = 1 # Upgrade this if incompatible changes are made
12
+ # in the class so that file loading of old files can still
13
+ # be checked against.
14
+
15
+ attr_reader :zoom # Current zooming factor
16
+ attr_accessor :filename # Filename of current map (if any)
17
+ attr_reader :modified # Was map modified since being loaded?
18
+ attr_accessor :navigation # Map is navigation mode (no new nodes can be created)
19
+ attr_accessor :options # Map options
20
+ attr_reader :window # Fox Window for this map
21
+ attr :version # file format version
22
+
23
+ # pmap is a path map (a matrix or grid used for path finding).
24
+ # Rooms and paths are recorded there. Path finding is needed
25
+ # to draw complex connections (ie. those that are farther than one square)
26
+ # We now also use this for selecting of stuff, particularly complex paths.
27
+ attr :pmap
28
+
29
+ @@win = nil
30
+
31
+ @@cursor_arrow = nil
32
+ @@cursor_cross = nil
33
+ @@cursor_updown = nil
34
+
35
+
36
+ def _changed
37
+ create_pathmap
38
+ update_title
39
+ draw
40
+ end
41
+
42
+ def page=(x)
43
+ super
44
+ @complexConnection = false
45
+ create_pathmap
46
+ update_title
47
+ end
48
+
49
+
50
+ def previous_page
51
+ self.page = @page - 1
52
+ _changed
53
+ end
54
+
55
+ def next_page
56
+ self.page = @page + 1
57
+ _changed
58
+ end
59
+
60
+ def modified=(x)
61
+ @modified = true
62
+ _changed
63
+ end
64
+
65
+ def rename_page
66
+ @pages[@page].properties(self)
67
+ modified = true
68
+ end
69
+
70
+ def delete_page
71
+ w = FXWarningBox.new(@window, "Are you sure you want to delete this page?")
72
+ return if w.execute == 0
73
+
74
+ delete_page_at(@page)
75
+ modified = true
76
+ end
77
+
78
+ def new_page
79
+ @pages.push( FXPage.new )
80
+ @page = @pages.size - 1
81
+ end
82
+
83
+ #
84
+ # A simple debugging function that will spit out the path map with a
85
+ # very simple ascii representation.
86
+ #
87
+ def dump_pathmap
88
+ s = ' ' * @width + "\n"
89
+ m = s * @height
90
+ (0...@width).each { |x|
91
+ (0...@height).each { |y|
92
+ loc = y * (@width+1) + x
93
+ if @pmap[x][y].kind_of?(Connection)
94
+ m[loc] = '-'
95
+ elsif @pmap[x][y].kind_of?(Room)
96
+ m[loc] = 'R'
97
+ end
98
+ }
99
+ }
100
+ puts m
101
+ end
102
+
103
+ #
104
+ # Reinitialize the pathmap to an empty matrix
105
+ #
106
+ def empty_pathmap
107
+ # First, create an empty grid of width x height
108
+ @pmap = Array.new(@width)
109
+ (0...@width).each { |x|
110
+ @pmap[x] = Array.new(@height)
111
+ }
112
+ return pmap
113
+ end
114
+
115
+ #
116
+ # Recreate the pathmap based on rooms and connections
117
+ # This routine is used on loading a new map.
118
+ #
119
+ def create_pathmap
120
+ # First, create an empty grid of width x height
121
+ empty_pathmap # Hmm... needed? Probably not.
122
+ # Then, fill it in with all rooms...
123
+ @pages[@page].rooms.each { |r| @pmap[r.x][r.y] = r }
124
+ # And following, add all paths
125
+ @pages[@page].connections.each { |c| path_find(c) }
126
+ end
127
+
128
+ #
129
+ # Given a connection, clean its path from path map.
130
+ #
131
+ def clean_path(c)
132
+ c.gpts.each { |p| @pmap[p[0]][p[1]] = nil if @pmap[p[0]][p[1]] == c }
133
+ end
134
+
135
+ #
136
+ # Remove a connection from map, since path creation failed.
137
+ #
138
+ def remove_connection(c)
139
+ clean_path(c)
140
+ c.failed = true
141
+ status "Path for connection #{c} is blocked."
142
+ end
143
+
144
+ # Given a connection, create the path for it, if not a simple
145
+ # connection. Also, add the paths to pathmap.
146
+ def path_find(c)
147
+ unless c.complex?
148
+ c.pts = c.gpts = []
149
+ c.failed = false
150
+ return true
151
+ end
152
+
153
+ # Complex path... Generate points.
154
+ a = c.roomA
155
+ b = c.roomB
156
+
157
+ # First, check the neighboring starting/ending nodes in grid
158
+ # are empty. If not, we need to abort and remove path from list.
159
+ dirA = a.exits.index(c)
160
+ raise "connection not found #{c} at #{a}" unless dirA
161
+
162
+ dirB = b.exits.rindex(c)
163
+ raise "connection not found #{c} at #{b}" unless dirB
164
+
165
+ vA = FXRoom::DIR_TO_VECTOR[dirA]
166
+ vB = FXRoom::DIR_TO_VECTOR[dirB]
167
+
168
+ pA = [ a.x + vA[0], a.y + vA[1] ]
169
+ pB = [ b.x + vB[0], b.y + vB[1] ]
170
+
171
+ c.gpts = []
172
+ c.pts = []
173
+
174
+ # Check for the special case of looping path (path that begins and
175
+ # returns to same exit)
176
+ if a == b and dirA == dirB
177
+ pt = a.corner(c, 1, dirA)
178
+ n = 1.0 / Math.sqrt(vA[0] * vA[0] + vA[1] * vA[1])
179
+ vA = [ vA[0] * n, vA[1] * n ]
180
+ c.pts.push( [ pt[0], pt[1] ] )
181
+ pA = [ pt[0] + vA[0] * 20, pt[1] + vA[1] * 20 ]
182
+ c.pts.push( [pA[0], pA[1]] )
183
+ pB = [ pA[0] + vA[1] * 20, pA[1] - vA[0] * 20 ]
184
+ c.pts.push( [pB[0], pB[1]] )
185
+ pC = [ pB[0] - vA[0] * 20, pB[1] - vA[1] * 20 ]
186
+ c.pts.push( [pC[0], pC[1]] )
187
+ c.dir = Connection::AtoB
188
+ return true
189
+ end
190
+
191
+
192
+ if pA[0] < 0 or pA[0] >= @width or
193
+ pB[0] < 0 or pB[0] >= @width or
194
+ pA[1] < 0 or pA[1] >= @height or
195
+ pB[1] < 0 or pB[1] >= @height
196
+ remove_connection(c)
197
+ return false
198
+ end
199
+
200
+ c.failed = false
201
+
202
+ # We do two tries on path finding.
203
+ # On first attempt, we try to avoid both rooms and paths.
204
+ # That is, we always try to have no paths crossing each other.
205
+ #
206
+ # If this fails, we try a second time, but we try to avoid just
207
+ # rooms. In this second attempt, paths are allowed to intercross.
208
+ #
209
+ pt = pA.dup
210
+ 2.times { |try|
211
+ c.gpts = []
212
+
213
+ if try == 0
214
+ next if @pmap[pA[0]][pA[1]] or @pmap[pB[0]][pB[1]] # try again
215
+ else
216
+ if @pmap[pA[0]][pA[1]].kind_of?(Room) or
217
+ @pmap[pB[0]][pB[1]].kind_of?(Room)
218
+ remove_connection(c)
219
+ return false
220
+ end
221
+ end
222
+
223
+ # Okay, now we begin the real path algorithm...
224
+ # Add first point...
225
+ add_path_pt(c, pA[0], pA[1])
226
+
227
+ pt = pA.dup
228
+
229
+ # Okay, now we begin a simple A* path algorithm.
230
+ while pt[0] != pB[0] or pt[1] != pB[1]
231
+ # From point pt, get a list of free nodes, by looking all around it.
232
+ pts = []
233
+ FXRoom::DIR_TO_VECTOR.each_value { |d|
234
+ n = pt.dup
235
+ n[0] += d[0]
236
+ n[1] += d[1]
237
+ # Don't add point to list if we go out of map
238
+ if n[0] < 0 or n[0] >= @width or
239
+ n[1] < 0 or n[1] >= @height
240
+ next
241
+ end
242
+
243
+ # Now, check if pmap is an empty node.
244
+ if try == 1
245
+ next if @pmap[n[0]][n[1]].kind_of?(Room) or
246
+ @pmap[n[0]][n[1]] == c
247
+ else
248
+ next if @pmap[n[0]][n[1]]
249
+ end
250
+
251
+ # Ok, we do have an empty node. Add to list.
252
+ pts.push(n)
253
+ }
254
+
255
+ break if pts.empty?
256
+
257
+ # Okay, from all possible nodes, calculate best one, using
258
+ # the formula:
259
+ # F = G + H
260
+ # where G = movement cost to move from point to other point
261
+ # (10 or 14, depending if move is straight or diagonal)
262
+ # H = cost of square to shorten distance to destination.
263
+ # (heuristic, in this case using Manhattan distance)
264
+ # Next move is the grid node with lowest F cost.
265
+
266
+ f = []
267
+ pts.each { |x|
268
+ dx = x[0] - pt[0]
269
+ dy = x[1] - pt[1]
270
+ g = 10 # straight move G cost = 10 (ie. prefer straight moves)
271
+ if dx.abs + dy.abs > 1
272
+ g = 14 # diagonal move G cost = 14
273
+ end
274
+
275
+ dx = pB[0] - x[0]
276
+ dy = pB[1] - x[1]
277
+ h = dx * dx + dy * dy
278
+
279
+ f.push( h + g )
280
+ }
281
+
282
+ idx = f.index(f.min)
283
+ pt = pts[idx]
284
+ add_path_pt(c, pt[0], pt[1])
285
+ end # while
286
+
287
+
288
+ # Are we really there?
289
+ if pt[0] != pB[0] or pt[1] != pB[1]
290
+ # No, we must have failed...
291
+ clean_path(c)
292
+ c.gpts = []
293
+ next # go for next try
294
+ else
295
+ break
296
+ end
297
+ }
298
+
299
+ # If still not there, we cannot connect path, exit gracefully
300
+ if pt[0] != pB[0] or pt[1] != pB[1]
301
+ # If on second try, remove connection as invalid.
302
+ remove_connection(c)
303
+ return false
304
+ end
305
+
306
+
307
+ # Okay, we have a valid path.
308
+ # Create real path in display coordinates now...
309
+ # Start with a's corner
310
+ pt = a.corner(c, 1, dirA)
311
+ c.pts.push( [ pt[0], pt[1] ] )
312
+ # Then, add each grid point we calculated
313
+ c.gpts.each { |pt|
314
+ x = pt[0] * WW + WW / 2
315
+ y = pt[1] * HH + HH / 2
316
+ c.pts.push([x, y])
317
+ }
318
+ # And end with b's corner
319
+ pt = b.corner(c, 1, dirB)
320
+ c.failed = false
321
+ return c.pts.push([pt[0], pt[1]])
322
+ end
323
+
324
+ #
325
+ # Add a new path point to a connection
326
+ #
327
+ def add_path_pt( c, x, y )
328
+ @pmap[x][y] = c
329
+ c.gpts.push([x, y])
330
+ end
331
+
332
+ #
333
+ # Used for loading class with Marshal
334
+ #
335
+ def marshal_load(variables)
336
+ # if variables[1].kind_of?(Map)
337
+ # # old format
338
+ # @zoom = variables[0]
339
+ # tmpmap = variables[1]
340
+ # copy(tmpmap)
341
+ # @navigation = variables[2]
342
+ # @options = variables[3] if variables[3]
343
+ # else
344
+ @zoom = variables.shift
345
+ @navigation = variables.shift
346
+ @options = variables.shift
347
+ super
348
+ # end
349
+ @modified = false
350
+ end
351
+
352
+ #
353
+ # Used for saving class with Marshal
354
+ #
355
+ def marshal_dump
356
+ [ @zoom, @navigation, @options ] + super
357
+ end
358
+
359
+ #
360
+ # Used to copy relevant data from one (temporary) map to another
361
+ #
362
+ def copy(b)
363
+ super(b)
364
+ if b.kind_of?(FXMap)
365
+ @options = b.options if b.options
366
+ @filename = b.filename
367
+ self.zoom = b.zoom
368
+ end
369
+ end
370
+
371
+ #
372
+ # Function used to add a new room. x and y are absolute pixel positions
373
+ # in canvas.
374
+ #
375
+ def new_xy_room(x, y)
376
+ @modified = true
377
+ x = x / WW
378
+ y = y / HH
379
+ r = @pages[@page].new_room(x, y)
380
+ @pmap[x][y] = r
381
+
382
+ buf = FXMapperWindow::copy_buffer()
383
+ r.copy(buf) if buf
384
+ r.selected = true
385
+
386
+
387
+ if @options['Edit on Creation']
388
+ if not r.modal_properties(self)
389
+ @pages[@page].delete_room(r)
390
+ @pmap[x][y] = nil
391
+ return nil
392
+ end
393
+ end
394
+
395
+ return r
396
+ end
397
+
398
+
399
+ # Given a canvas (mouse) x/y position, return:
400
+ #
401
+ # false - arrow click
402
+ # true - room click
403
+ #
404
+ def click_type(x, y)
405
+ x = (x % WW).to_i
406
+ y = (y % HH).to_i
407
+
408
+ if x >= WS_2 and y >= HS_2 and
409
+ x <= (W + WS_2) and y <= (H + HS_2)
410
+ return true
411
+ else
412
+ return false
413
+ end
414
+ end
415
+
416
+ # Given an x and y canvas position, return room object,
417
+ # complex connection or nil
418
+ def to_room(x,y)
419
+ xx = x / WW
420
+ yy = y / HH
421
+ return @pmap[xx][yy]
422
+ end
423
+
424
+ # Given a mouse click x/y position, return object(s) if any or nil
425
+ def to_object(x, y)
426
+
427
+ exitA = get_quadrant(x, y)
428
+ unless exitA
429
+ # Not in arrow section, return element based on pmap
430
+ # can be a room or complex arrow connection.
431
+ xx = x / WW
432
+ yy = y / HH
433
+ return nil if xx >= @width or yy >= @height
434
+ return @pmap[xx][yy]
435
+ else
436
+ # Possible arrow
437
+ @pages[@page].connections.each { |c|
438
+ a = c.roomA
439
+ b = c.roomB
440
+ next if not b # Complex connection in progress
441
+
442
+ if c.gpts.size > 0
443
+ [a, b].each { |r|
444
+ next if not r
445
+ dir = r.exits.index(c)
446
+ x1, y1 = r.corner(c, 1, dir)
447
+ v = FXRoom::DIR_TO_VECTOR[dir]
448
+ x2 = x1 + v[0] * WS
449
+ y2 = y1 + v[1] * HS
450
+
451
+ if x1 == x2
452
+ x1 -= W / 2
453
+ x2 += W / 2
454
+ end
455
+ if y1 == y2
456
+ y1 -= H / 2
457
+ y2 += H / 2
458
+ end
459
+ if x >= x1 and x <= x2 and
460
+ y >= y1 and y < y2
461
+ return c
462
+ end
463
+ }
464
+ else
465
+ x1, y1 = a.corner(c, 1, a.exits.index(c))
466
+ x2, y2 = b.corner(c, 1, b.exits.rindex(c))
467
+ x1, x2 = x2, x1 if x2 < x1
468
+ y1, y2 = y2, y1 if y2 < y1
469
+ if x1 == x2
470
+ x1 -= W / 2
471
+ x2 += W / 2
472
+ end
473
+ if y1 == y2
474
+ y1 -= H / 2
475
+ y2 += H / 2
476
+ end
477
+ if x >= x1 and x <= x2 and
478
+ y >= y1 and y < y2
479
+ return c
480
+ end
481
+ end
482
+ }
483
+
484
+ # Then, get "rooms" being connected to check if we get
485
+ # a complex connection instead.
486
+ roomA, roomB, a, b = quadrant_to_rooms( exitA, x, y )
487
+ return roomA if roomA.kind_of?(Connection)
488
+ return roomB if roomB.kind_of?(Connection)
489
+ end
490
+
491
+ return nil
492
+ end
493
+
494
+ def update_title
495
+ title = @name.dup
496
+ if @navigation
497
+ title << " [Read Only]"
498
+ end
499
+ title << " Zoom: %.3f" % @zoom
500
+ title << " Page #{@page+1} of #{@pages.size}"
501
+ title << " #{@pages[@page].name}"
502
+ @window.title = title
503
+ end
504
+
505
+ # Change zoom factor of map. Rebuild fonts and canvas sizes.
506
+ def zoom=(value)
507
+ @zoom = ("%.2f" % value).to_f
508
+ # Create the font
509
+ fontsize = (11 * @zoom).to_i
510
+
511
+ if @window
512
+ @font = FXFont.new(@window.getApp, @options['Font Text'], fontsize)
513
+ @font.create
514
+
515
+ @objfont = FXFont.new(@window.getApp, @options['Font Objects'],
516
+ (fontsize * 0.75).to_i)
517
+ @objfont.create
518
+
519
+ width = (WW * @width * @zoom).to_i
520
+ height = (HH * @height * @zoom).to_i
521
+ @canvas.width = width
522
+ @canvas.height = height
523
+
524
+ # Then, create an off-screen image with that same size for double
525
+ # buffering
526
+ @image.destroy
527
+ @image = FXBMPImage.new(@window.getApp, nil, IMAGE_SHMI|IMAGE_SHMP,
528
+ width, height)
529
+ @image.create
530
+ update_title
531
+ end
532
+
533
+ end
534
+
535
+ # Given a mouse x/y position to WS/HS, return an index
536
+ # indicating what quadrant it belongs to.
537
+ def get_quadrant(ax, ay)
538
+ # First get relative x/y position
539
+ x = ax % WW
540
+ y = ay % HH
541
+
542
+ quadrant = nil
543
+ if x < WS_2
544
+ #left
545
+ if y < HS_2
546
+ # top
547
+ quadrant = 7
548
+ elsif y > H + HS_2
549
+ # bottom
550
+ quadrant = 5
551
+ else
552
+ # center
553
+ quadrant = 6
554
+ end
555
+ elsif x > W + WS_2
556
+ # right
557
+ if y < HS_2
558
+ # top
559
+ quadrant = 1
560
+ elsif y > H + HS_2
561
+ # bottom
562
+ quadrant = 3
563
+ else
564
+ # center
565
+ quadrant = 2
566
+ end
567
+ else
568
+ #center
569
+ if y < HS_2
570
+ # top
571
+ quadrant = 0
572
+ elsif y > H + HS_2
573
+ # bottom
574
+ quadrant = 4
575
+ else
576
+ # center
577
+ quadrant = nil
578
+ end
579
+ end
580
+
581
+
582
+ return quadrant
583
+ end
584
+
585
+ # Given an x,y absolute position corresponding to a connection,
586
+ # return connected rooms (if any).
587
+ def quadrant_to_rooms( q, x, y )
588
+ maxX = @width * WW
589
+ maxY = @height * HH
590
+
591
+ # First check if user tried adding a connection
592
+ # at the edges of the map. If so, return empty stuff.
593
+ if x < WS_2 or y < HS_2 or
594
+ x > maxX - WS_2 or y > maxY - HS_2
595
+ return [nil, nil, nil, nil]
596
+ end
597
+
598
+ x1 = x2 = x
599
+ y1 = y2 = y
600
+
601
+ case q
602
+ when 0, 4
603
+ y1 -= HS
604
+ y2 += HS
605
+ when 1, 5
606
+ x1 -= WS
607
+ x2 += WS
608
+ y1 += HS
609
+ y2 -= HS
610
+ when 2, 6
611
+ x1 -= WS
612
+ x2 += WS
613
+ when 3, 7
614
+ x1 -= WS
615
+ y1 -= HS
616
+ x2 += WS
617
+ y2 += HS
618
+ end
619
+
620
+ case q
621
+ when 0, 5, 6, 7
622
+ x1, x2 = x2, x1
623
+ y1, y2 = y2, y1
624
+ end
625
+
626
+ roomA = to_room(x1, y1)
627
+ roomB = to_room(x2, y2)
628
+ # Oops, user tried to create rooms where we already
629
+ # have a complex connection. Don't create anything, then.
630
+ if roomA.kind_of?(Connection) or roomB.kind_of?(Connection)
631
+ return [roomA, roomB, nil, nil]
632
+ end
633
+
634
+ return [roomA, roomB, [x1, y1], [x2, y2]]
635
+ end
636
+
637
+ #
638
+ # Add a new complex connection (or its first point)
639
+ #
640
+ def new_complex_connection( x, y )
641
+ exitA = get_quadrant(x, y)
642
+ unless exitA
643
+ raise "not a connection"
644
+ end
645
+ roomA, roomB, a, b = quadrant_to_rooms( exitA, x, y )
646
+ return unless a and roomA # User click outside map or not near room
647
+
648
+ if @complexConnection.kind_of?(TrueClass)
649
+ @complexConnection = [roomA, exitA]
650
+ c = new_connection( roomA, exitA, nil )
651
+ status "Click on other room exit in complex connection."
652
+ else
653
+ @pages[@page].delete_connection_at(-1)
654
+ c = new_connection( @complexConnection[0],
655
+ @complexConnection[1], roomA, exitA )
656
+ if path_find(c) # Do A* path finding to connect both exits
657
+ @modified = true
658
+ status "Complex connection done."
659
+ else
660
+ @pages[@page].delete_connection_at(-1)
661
+ end
662
+ draw
663
+ @complexConnection = nil
664
+ end
665
+ end
666
+
667
+ #
668
+ # Add a new room connection among contiguous rooms.
669
+ #
670
+ def new_xy_connection( x, y )
671
+ exitA = get_quadrant(x, y)
672
+ unless exitA
673
+ raise "not a connection"
674
+ end
675
+
676
+ # Then, get rooms being connected
677
+ roomA, roomB, a, b = quadrant_to_rooms( exitA, x, y )
678
+ return unless a # User click outside map
679
+
680
+ if @options['Create on Connection']
681
+ unless roomA
682
+ roomA = new_xy_room( a[0], a[1] )
683
+ end
684
+
685
+ unless roomB
686
+ roomB = new_xy_room( b[0], b[1] )
687
+ end
688
+ end
689
+
690
+ return nil unless roomA and roomB
691
+
692
+ if roomA == roomB
693
+ raise "error: same room connection"
694
+ end
695
+
696
+ @modified = true
697
+ if roomA and exitA
698
+ # get old connection
699
+ if roomA[exitA]
700
+ c = roomA[exitA]
701
+ delete_connection(c) if c.roomB == nil
702
+ end
703
+ exitB = (exitA + 4) % 8
704
+ if roomB[exitB]
705
+ c = roomB[exitB]
706
+ delete_connection(c) if c.roomB == nil
707
+ end
708
+ end
709
+
710
+ new_connection( roomA, exitA, roomB )
711
+ end
712
+
713
+ #
714
+ # Handle mouse button double clicks in canvas
715
+ #
716
+ def double_click_cb(selection, event)
717
+ return unless selection
718
+ if selection.kind_of?(FXRoom) or selection.kind_of?(FXConnection)
719
+ selection.properties( self, event )
720
+ end
721
+ end
722
+
723
+ ZOOM_SEQUENCE = [
724
+ 0.4,
725
+ 0.5,
726
+ 0.6,
727
+ 0.7,
728
+ 0.8,
729
+ 0.9,
730
+ 1.0,
731
+ 1.1,
732
+ 1.2,
733
+ ]
734
+
735
+ # Self-explanatory.
736
+ def zoom_out
737
+ if @zoom > 0.25
738
+ self.zoom -= 0.1
739
+ end
740
+ end
741
+
742
+ # Self-explanatory.
743
+ def zoom_in
744
+ if @zoom < 1.25
745
+ self.zoom += 0.1
746
+ end
747
+ end
748
+
749
+ # Spit out a new message to the status line.
750
+ def status(msg)
751
+ mw = @window.parent.parent
752
+ statusbar = mw.children.find() { |x| x.kind_of?(FXStatusBar) }
753
+ s = statusbar.statusLine
754
+ s.normalText = s.text = msg
755
+ end
756
+
757
+ #
758
+ # Show some help status in status line based on cursor position
759
+ #
760
+ def help_cb(sender, sel, event)
761
+ return if @complexConnection
762
+ x = (event.last_x / @zoom).to_i
763
+ y = (event.last_y / @zoom).to_i
764
+
765
+ sel = to_object(x, y)
766
+ if sel
767
+ @canvas.defaultCursor = @@cursor_arrow
768
+ if sel.kind_of?(Room)
769
+ status "Click to select and move. Double click to edit room."
770
+ elsif sel.kind_of?(Connection)
771
+ status "Click to change direction of connection."
772
+ end
773
+ else
774
+ if click_type(x, y)
775
+ @canvas.defaultCursor = @@cursor_cross
776
+ status "Click to create new room."
777
+ else
778
+ q = get_quadrant(x, y)
779
+ if q == 0 or q == 4
780
+ @canvas.defaultCursor = @@cursor_updown
781
+ elsif q == 2 or q == 6
782
+ @canvas.defaultCursor = @@cursor_leftright
783
+ else
784
+ @canvas.defaultCursor = @@cursor_arrow
785
+ end
786
+ status "Click to create new connection."
787
+ end
788
+ end
789
+ end
790
+
791
+ #
792
+ # zoom in/out based on mousewheel, keeping position relative
793
+ # to cursor position
794
+ #
795
+ def mousewheel_cb(sender, sel, event)
796
+ pos = @scrollwindow.position
797
+ pos = [ pos[0] / @zoom, pos[1] / @zoom ]
798
+ x = (event.last_x / @zoom).to_i
799
+ y = (event.last_y / @zoom).to_i
800
+ case event.code
801
+ when -120
802
+ zoom_out
803
+ # pos[0] -= x / 2
804
+ # pos[1] -= y / 2
805
+ pos = [ pos[0] * @zoom, pos[1] * @zoom ]
806
+ @scrollwindow.setPosition(pos[0], pos[1])
807
+ when 120
808
+ zoom_in
809
+
810
+ #pos[0] = -x # * @zoom * 2
811
+ #pos[1] = -y # * @zoom * 2
812
+
813
+ # pos = [ pos[0], pos[1] ]
814
+ # pos = [ pos[0] * @zoom, pos[1] * @zoom ]
815
+ @scrollwindow.setPosition(pos[0], pos[1])
816
+ end
817
+ # x = (x * @zoom).to_i
818
+ # y = (y * @zoom).to_i
819
+ # if x > @canvas.width
820
+ # x = @canvas.width
821
+ # end
822
+ # if y > @canvas.height
823
+ # y = @canvas.height
824
+ # end
825
+
826
+ # pos = [ pos[0] - x, pos[1] - y ]
827
+ # p "SET: #{pos.join(',')}"
828
+
829
+ # # @scrollwindow.setPosition(pos[0], pos[1])
830
+ # p @scrollwindow.position
831
+ draw
832
+ end
833
+
834
+ #
835
+ # Handle middle mouse button click in canvas
836
+ #
837
+ def mmb_click_cb(server, sel, event)
838
+ @canvas.grab
839
+ @dx = @dy = 0
840
+ @mouseButton = MIDDLEBUTTON
841
+ end
842
+
843
+ #
844
+ # Select all rooms within (drag) rectangle
845
+ # TODO: Should add connections too
846
+ #
847
+ def select_rectangle(x1, y1, x2, y2)
848
+ x1, x2 = x2, x1 if x2 < x1
849
+ y1, y2 = y2, y1 if y2 < y1
850
+
851
+ x = x1 * zoom
852
+ y = y1 * zoom
853
+ w = x2 - x1
854
+ h = y2 - y1
855
+
856
+ x1 = (x1 / WW).floor
857
+ y1 = (y1 / HH).floor
858
+ x2 = (x2 / WW).ceil
859
+ y2 = (y2 / HH).ceil
860
+
861
+ @pages[@page].rooms.each { |r|
862
+ if r.x >= x1 and r.x <= x2 and
863
+ r.y >= y1 and r.y <= y2
864
+ r.selected = true
865
+ else
866
+ r.selected = false
867
+ end
868
+ }
869
+ draw
870
+ dc = FXDCWindow.new(@canvas)
871
+ dc.drawRectangle(x, y, w * @zoom, h * @zoom)
872
+ dc.end
873
+ end
874
+
875
+ #
876
+ # Handle mouse motion in canvas
877
+ #
878
+ def motion_cb(server, sel, event)
879
+ if @mouseButton == MIDDLEBUTTON
880
+ @canvas.dragCursor = @@cursor_move
881
+ pos = @scrollwindow.position
882
+ dx = event.last_x - event.win_x
883
+ dy = event.last_y - event.win_y
884
+ if dx != 0 or dy != 0 and not (dx == @dx and dy == @dy)
885
+ pos[0] += dx
886
+ pos[1] += dy
887
+ @dx = dx
888
+ @dy = dy
889
+ @scrollwindow.setPosition(pos[0], pos[1])
890
+ end
891
+ elsif @mouseButton == LEFTBUTTON
892
+ x = (event.last_x / @zoom).to_i
893
+ y = (event.last_y / @zoom).to_i
894
+ select_rectangle( @dx, @dy, x, y )
895
+ else
896
+ help_cb(server, sel, event)
897
+ end
898
+ end
899
+
900
+ #
901
+ # Handle release of middle mouse button
902
+ #
903
+ def mmb_release_cb(server, sel, event)
904
+ if @mouseButton
905
+ @canvas.ungrab
906
+ @mouseButton = nil
907
+ @canvas.dragCursor = @@cursor_arrow
908
+ draw
909
+ end
910
+ end
911
+
912
+ #
913
+ # Clear rooms/connections selected
914
+ #
915
+ def clear_selection
916
+ @pages[@page].rooms.each { |r| r.selected = false }
917
+ @pages[@page].connections.each { |r| r.selected = false }
918
+ end
919
+
920
+ #
921
+ # Handle left mouse button click
922
+ #
923
+ def lmb_click_cb(sender, sel, event)
924
+ x = (event.last_x / @zoom).to_i
925
+ y = (event.last_y / @zoom).to_i
926
+
927
+ if event.state & ALTMASK != 0
928
+ mmb_click_cb(sender, sel, event)
929
+ return
930
+ end
931
+
932
+ selection = to_object(x, y)
933
+
934
+ if event.state & SHIFTMASK != 0
935
+ @mouseButton = LEFTBUTTON
936
+ @canvas.grab
937
+ @dx, @dy = [ x, y ]
938
+ return if not selection
939
+ end
940
+
941
+
942
+ if event.click_count == 2
943
+ double_click_cb(selection, event)
944
+ return
945
+ end
946
+
947
+ unless selection
948
+ clear_selection
949
+
950
+ # If in navigation mode, we don't allow user to modify map.
951
+ return if @navigation
952
+
953
+ # if we did not select anything, check to see if we
954
+ # clicked in a room area or connection area.
955
+ if click_type(x, y)
956
+ return if @complexConnection
957
+ # Add a new room
958
+ roomB = @pages[@page].rooms[-1]
959
+ roomA = new_xy_room( x, y )
960
+ if roomB and roomA and @options['Automatic Connection']
961
+ # check to see if rooms are next to each other
962
+ # if so, try to connect them (assuming there's no connection there
963
+ # already).
964
+ exitB = roomB.next_to?(roomA)
965
+ if exitB and roomB.exits[exitB] == nil
966
+ new_connection( roomB, exitB, roomA )
967
+ end
968
+ end
969
+ else
970
+ # Add a new connection
971
+ if @complexConnection
972
+ new_complex_connection(x, y)
973
+ else
974
+ # Add a new simple connection (plus rooms if needed)
975
+ if event.state & CONTROLMASK != 0
976
+ exitA = get_quadrant(x, y)
977
+ roomA, roomB, a, b = quadrant_to_rooms( exitA, x, y )
978
+ if not roomA
979
+ new_xy_connection( x, y )
980
+ else
981
+ @complexConnection = [roomA, exitA]
982
+ new_connection( roomA, exitA, nil )
983
+ new_complex_connection( x, y )
984
+ end
985
+ else
986
+ new_xy_connection( x, y )
987
+ end
988
+ end
989
+ end
990
+ else
991
+ if selection.kind_of?(Connection) and selection.selected
992
+ # Toggle arrow direction
993
+ selection.toggle_direction
994
+ draw
995
+ return
996
+ else
997
+ if event.state & SHIFTMASK == 0 and
998
+ event.state & CONTROLMASK == 0
999
+ clear_selection
1000
+ end
1001
+ # Select the stuff
1002
+ selection.selected = true
1003
+ end
1004
+ end
1005
+ draw(event)
1006
+ end
1007
+
1008
+
1009
+ def close_cb()
1010
+ if @modified
1011
+ dlg = FXDialogBox.new( @window.parent, "Warning",
1012
+ DECOR_ALL,
1013
+ 0, 0, 400, 130)
1014
+ # Frame
1015
+ s = FXVerticalFrame.new(dlg,
1016
+ LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
1017
+
1018
+ f = FXHorizontalFrame.new(s, LAYOUT_SIDE_TOP|LAYOUT_FILL_X|LAYOUT_FILL_Y)
1019
+
1020
+ font = FXFont.new(@window.getApp, "Helvetica", 30)
1021
+ font.create
1022
+ oops = FXLabel.new(f, "!", nil, 0, LAYOUT_SIDE_LEFT|LAYOUT_FILL_X|
1023
+ LAYOUT_CENTER_Y)
1024
+ oops.frameStyle = FRAME_RAISED|FRAME_THICK
1025
+ oops.baseColor = 'dark grey'
1026
+ oops.textColor = 'red'
1027
+ oops.padLeft = oops.padRight = 15
1028
+ oops.shadowColor = 'black'
1029
+ oops.borderColor = 'white'
1030
+ oops.font = font
1031
+
1032
+ FXLabel.new(f, "\n#{@name} was modified.\n" +
1033
+ "Should I save the changes before closing?",
1034
+ nil, 0)
1035
+
1036
+ # Separator
1037
+ FXHorizontalSeparator.new(s,
1038
+ LAYOUT_SIDE_TOP|LAYOUT_FILL_X|SEPARATOR_GROOVE)
1039
+
1040
+ # Bottom buttons
1041
+ buttons = FXHorizontalFrame.new(s,
1042
+ LAYOUT_SIDE_BOTTOM|FRAME_NONE|
1043
+ LAYOUT_FILL_X|PACK_UNIFORM_WIDTH)
1044
+ # Accept
1045
+ yes = FXButton.new(buttons, "&Yes", nil, dlg, FXDialogBox::ID_ACCEPT,
1046
+ FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_X|
1047
+ LAYOUT_RIGHT|LAYOUT_CENTER_Y)
1048
+ yes.connect(SEL_COMMAND) {
1049
+ dlg.close
1050
+ if save
1051
+ @window.close
1052
+ return true
1053
+ else
1054
+ return false
1055
+ end
1056
+ }
1057
+ FXButton.new(buttons, "&No", nil, dlg, FXDialogBox::ID_ACCEPT,
1058
+ FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_RIGHT|LAYOUT_CENTER_Y)
1059
+
1060
+ # Cancel
1061
+ FXButton.new(buttons, "&Cancel", nil, dlg, FXDialogBox::ID_CANCEL,
1062
+ FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_RIGHT|LAYOUT_CENTER_Y)
1063
+ yes.setDefault
1064
+ yes.setFocus
1065
+
1066
+ return false if dlg.execute == 0
1067
+ end
1068
+ @window.close
1069
+ return true
1070
+ end
1071
+
1072
+
1073
+ def _make_cursors
1074
+ @@cursor_move = FXCursor.new(@window.getApp, CURSOR_MOVE)
1075
+ @@cursor_move.create
1076
+
1077
+ @@cursor_arrow = FXCursor.new(@window.getApp, CURSOR_ARROW)
1078
+ @@cursor_arrow.create
1079
+
1080
+ @@cursor_cross = FXCursor.new(@window.getApp, CURSOR_CROSS)
1081
+ @@cursor_cross.create
1082
+
1083
+ @@cursor_updown = FXCursor.new(@window.getApp, CURSOR_UPDOWN)
1084
+ @@cursor_updown.create
1085
+
1086
+ @@cursor_leftright = FXCursor.new(@window.getApp, CURSOR_LEFTRIGHT)
1087
+ @@cursor_leftright.create
1088
+ end
1089
+
1090
+ def _make_widgets
1091
+ @scrollwindow = FXScrollWindow.new(@window,
1092
+ SCROLLERS_NORMAL|SCROLLERS_TRACK)
1093
+ width = WW * @width
1094
+ height = HH * @height
1095
+
1096
+ @canvasFrame = FXVerticalFrame.new(@scrollwindow,
1097
+ FRAME_SUNKEN|LAYOUT_FILL_X|
1098
+ LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT)
1099
+
1100
+ # First, create an off-screen image for drawing and double buffering
1101
+ @image = FXBMPImage.new(@window.getApp, nil, IMAGE_SHMI|IMAGE_SHMP,
1102
+ width, height)
1103
+ @image.create
1104
+
1105
+ # Then create canvas
1106
+ @canvas = FXCanvas.new(@canvasFrame, nil, 0,
1107
+ LAYOUT_FIX_WIDTH|LAYOUT_FIX_HEIGHT|
1108
+ LAYOUT_TOP|LAYOUT_LEFT,
1109
+ 0, 0, width, height)
1110
+
1111
+ @canvas.connect(SEL_PAINT) do |sender, sel, event|
1112
+ draw(event)
1113
+ end
1114
+ @canvas.backColor = @options['BG Color']
1115
+
1116
+ @canvas.connect(SEL_MOUSEWHEEL, method(:mousewheel_cb))
1117
+ @canvas.connect(SEL_LEFTBUTTONPRESS, method(:lmb_click_cb))
1118
+ @canvas.connect(SEL_LEFTBUTTONRELEASE, method(:mmb_release_cb))
1119
+ @canvas.connect(SEL_MOTION, method(:motion_cb))
1120
+ @canvas.connect(SEL_MIDDLEBUTTONPRESS, method(:mmb_click_cb))
1121
+ @canvas.connect(SEL_MIDDLEBUTTONRELEASE, method(:mmb_release_cb))
1122
+ @canvas.connect(SEL_KEYPRESS, method(:keypress_cb))
1123
+ end
1124
+
1125
+ def initialize(name, parent = nil, default_options = nil,
1126
+ icon = nil, menu = nil, mode = nil,
1127
+ x = 0, y = 0, w = 0, h = 0)
1128
+ super(name)
1129
+ @navigation = false
1130
+ if parent
1131
+ @window = FXMDIChild.new(parent, name, icon, menu, mode, x, y, w, h)
1132
+ @options = default_options
1133
+ _make_cursors if not @@cursor_arrow
1134
+ _make_widgets
1135
+ end
1136
+ empty_pathmap
1137
+ self.zoom = 1
1138
+ end
1139
+
1140
+
1141
+ #
1142
+ # Handle deleting selected rooms and/or connections
1143
+ #
1144
+ def delete_selected
1145
+ rooms = @pages[@page].rooms.find_all { |r| r.selected }
1146
+ conns = @pages[@page].connections.find_all { |c| c.selected }
1147
+ ############################
1148
+ # First, handle rooms...
1149
+ ############################
1150
+ # Remove rooms from path map
1151
+ rooms.each { |r| @pmap[r.x][r.y] = nil }
1152
+ # Remove rooms from current page in map
1153
+ @pages[@page].rooms -= rooms
1154
+ # Add any connections pointing to removed rooms as connection to remove
1155
+ rooms.each { |r|
1156
+ conns += r.exits.find_all { |e| e != nil }
1157
+ }
1158
+
1159
+ #########################
1160
+ # Now, handle connections
1161
+ #########################
1162
+ conns.uniq!
1163
+
1164
+ # Remove connections from path map
1165
+ conns.each { |c| clean_path(c) }
1166
+ # Remove connections from current page in map
1167
+ @pages[@page].connections -= conns
1168
+ # Remove room exits pointing to any removed connection
1169
+ conns.each { |c|
1170
+ a = c.roomA
1171
+ b = c.roomB
1172
+ a[a.exits.index(c)] = nil
1173
+ idx = b.exits.rindex(c)
1174
+ b[idx] = nil if idx
1175
+ }
1176
+
1177
+ # Recreate pathmap. We need to recreate pathmap for all paths
1178
+ # as the removal of one path may help shorten the path of another.
1179
+ create_pathmap
1180
+
1181
+ draw()
1182
+ end
1183
+
1184
+ #
1185
+ # Start a complex connection
1186
+ #
1187
+ def complex_connection
1188
+ return if @complexConnection or @navigation
1189
+ @complexConnection = true
1190
+ status "Click on first room exit in complex connection."
1191
+ end
1192
+
1193
+ #
1194
+ # Given a selection of rooms, clear all of them from path map
1195
+ #
1196
+ def clean_room_selection(selection)
1197
+ selection.each { |r|
1198
+ @pmap[r.x][r.y] = nil
1199
+ clean_exits(r)
1200
+ }
1201
+ end
1202
+
1203
+ #
1204
+ # Given a selection of rooms, clear all of them from path map
1205
+ #
1206
+ def store_room_selection(selection)
1207
+ selection.each { |r|
1208
+ @pmap[r.x][r.y] = r
1209
+ }
1210
+ update_exits(selection)
1211
+ end
1212
+
1213
+ #
1214
+ # Clean all paths from path map for a room
1215
+ #
1216
+ def clean_exits(room)
1217
+ room.exits.each { |c|
1218
+ next if not c
1219
+ clean_path(c)
1220
+ }
1221
+ end
1222
+
1223
+
1224
+ #
1225
+ # Find and update all paths in path map for a room
1226
+ #
1227
+ def update_exits(selection)
1228
+ # We have to path_find all connections in the same order
1229
+ # as they are stored in map, and not in how they are in the room
1230
+ # so that there is consistency in path finding
1231
+ # conns = []
1232
+ # selection.each { |room|
1233
+ # room.exits.each { |c|
1234
+ # conns << c
1235
+ # }
1236
+ # }
1237
+
1238
+ # @pages[@page].connections.each { |c|
1239
+ # next if not conns.include?(c)
1240
+ # path_find(c)
1241
+ # }
1242
+ create_pathmap
1243
+ @modified = true
1244
+ end
1245
+
1246
+ #
1247
+ # Handle a keypress
1248
+ #
1249
+ def keypress_cb( server, sel, event)
1250
+ case event.code
1251
+ when KEY_Escape
1252
+ if @complexConnection
1253
+ if @complexConnection.kind_of?(Array)
1254
+ @pages[@page].delete_connection_at(-1)
1255
+ status "Complex connection aborted."
1256
+ draw
1257
+ end
1258
+ @complexConnection = false
1259
+ end
1260
+ when KEY_BackSpace, KEY_Delete
1261
+ return if @navigation
1262
+ delete_selected
1263
+ when KEY_c
1264
+ if event.state & CONTROLMASK != 0
1265
+ FXMapperWindow::copy_selected(self)
1266
+ draw
1267
+ end
1268
+ when KEY_v
1269
+ if event.state & CONTROLMASK != 0
1270
+ FXMapperWindow::paste_selected(self)
1271
+ @modified = true
1272
+ draw
1273
+ end
1274
+ when KEY_x
1275
+ return if @navigation
1276
+ if event.state & CONTROLMASK != 0
1277
+ FXMapperWindow::cut_selected(self)
1278
+ @modified = true
1279
+ draw
1280
+ else
1281
+ complex_connection
1282
+ end
1283
+ when KEY_Up
1284
+ return if @navigation
1285
+ selection = @pages[@page].rooms.find_all { |r| r.selected }
1286
+ return if selection.empty?
1287
+ clean_room_selection(selection)
1288
+ # Check that all nodes can be moved up
1289
+ selection.each { |r|
1290
+ n = @pmap[r.x][r.y-1]
1291
+ if r.y == 0 or n.kind_of?(Room)
1292
+ store_room_selection(selection)
1293
+ status "Cannot move selection up."
1294
+ return
1295
+ end
1296
+ }
1297
+ selection.each { |r|
1298
+ r.y -= 1
1299
+ @pmap[r.x][r.y] = r
1300
+ }
1301
+ update_exits(selection)
1302
+ draw
1303
+ when KEY_Down
1304
+ return if @navigation
1305
+ selection = @pages[@page].rooms.find_all { |r| r.selected }
1306
+ return if selection.empty?
1307
+ clean_room_selection(selection)
1308
+ # Check that all nodes can be moved up
1309
+ selection.each { |r|
1310
+ n = @pmap[r.x][r.y+1]
1311
+ if r.y+1 == @height or n.kind_of?(Room)
1312
+ store_room_selection(selection)
1313
+ status "Cannot move selection down."
1314
+ return
1315
+ end
1316
+ }
1317
+ selection.each { |r|
1318
+ r.y += 1
1319
+ @pmap[r.x][r.y] = r
1320
+ }
1321
+ update_exits(selection)
1322
+ draw
1323
+ when KEY_Left
1324
+ return if @navigation
1325
+ selection = @pages[@page].rooms.find_all { |r| r.selected }
1326
+ return if selection.empty?
1327
+ # Check that all nodes can be moved up
1328
+ clean_room_selection(selection)
1329
+ selection.each { |r|
1330
+ n = @pmap[r.x-1][r.y]
1331
+ if r.x == 0 or n.kind_of?(Room)
1332
+ store_room_selection(selection)
1333
+ status "Cannot move selection left."
1334
+ return
1335
+ end
1336
+ }
1337
+ selection.each { |r|
1338
+ r.x -= 1
1339
+ @pmap[r.x][r.y] = r
1340
+ }
1341
+ update_exits(selection)
1342
+ draw
1343
+ when KEY_Right
1344
+ return if @navigation
1345
+ selection = @pages[@page].rooms.find_all { |r| r.selected }
1346
+ return if selection.empty?
1347
+ # Check that all nodes can be moved up
1348
+ clean_room_selection(selection)
1349
+ selection.each { |r|
1350
+ n = @pmap[r.x+1][r.y]
1351
+ if r.x+1 == @width or n.kind_of?(Room)
1352
+ store_room_selection(selection)
1353
+ status "Cannot move selection right."
1354
+ return
1355
+ end
1356
+ }
1357
+ selection.each { |r|
1358
+ r.x += 1
1359
+ @pmap[r.x][r.y] = r
1360
+ }
1361
+ update_exits(selection)
1362
+ draw
1363
+ end
1364
+ end
1365
+
1366
+ #
1367
+ # Draw template of diagonal connections in grid background
1368
+ #
1369
+ def draw_diagonal_connections(dc, event)
1370
+ ww = WW * @zoom
1371
+ hh = HH * @zoom
1372
+
1373
+ w = W * @zoom
1374
+ h = H * @zoom
1375
+
1376
+ ws = WS * @zoom
1377
+ hs = HS * @zoom
1378
+
1379
+ ws_2 = WS_2 * @zoom
1380
+ hs_2 = HS_2 * @zoom
1381
+
1382
+ maxy = @height - 1
1383
+ maxx = @width - 1
1384
+
1385
+ (0...@height).each { |yy|
1386
+ (0...@width).each { |xx|
1387
+ next if @pmap[xx][yy].kind_of?(Connection)
1388
+ x = xx * ww
1389
+ y = yy * hh
1390
+
1391
+ if yy < maxy and xx < maxx
1392
+ # First, draw \
1393
+ x1 = x + w + ws_2
1394
+ y1 = y + h + hs_2
1395
+
1396
+ x2 = x1 + ws
1397
+ y2 = y1 + hs
1398
+ dc.drawLine( x1, y1, x2, y2 )
1399
+
1400
+ end
1401
+
1402
+ if yy < maxy and xx > 0 and xx <= maxx
1403
+ # Then, draw /
1404
+ x1 = x + ws_2
1405
+ y1 = y + h + hs_2
1406
+
1407
+ x2 = x1 - ws
1408
+ y2 = y1 + hs
1409
+ dc.drawLine( x2, y2, x1, y1 )
1410
+ end
1411
+ }
1412
+ }
1413
+ end
1414
+
1415
+ #
1416
+ # Draw template of straight connections in grid background
1417
+ #
1418
+ def draw_straight_connections(dc, event)
1419
+ ww = WW * @zoom
1420
+ hh = HH * @zoom
1421
+
1422
+ w = W * @zoom
1423
+ h = H * @zoom
1424
+
1425
+ ws_2 = WS_2 * @zoom
1426
+ hs_2 = HS_2 * @zoom
1427
+
1428
+ # First, draw horizontal lines
1429
+ (0...@height).each { |yy|
1430
+ (0..@width-2).each { |xx|
1431
+ next if @pmap[xx][yy].kind_of?(Connection) or
1432
+ @pmap[xx+1][yy].kind_of?(Connection)
1433
+ x1 = xx * ww + w + ws_2
1434
+ x2 = (xx + 1) * ww + ws_2
1435
+ y1 = yy * hh + h / 2 + hs_2
1436
+
1437
+ dc.drawLine( x1, y1, x2, y1 )
1438
+ }
1439
+ }
1440
+
1441
+ # Then, draw vertical lines
1442
+ (0...@width).each { |xx|
1443
+ (0..@height-2).each { |yy|
1444
+ next if @pmap[xx][yy].kind_of?(Connection) or
1445
+ @pmap[xx][yy+1].kind_of?(Connection)
1446
+ x1 = xx * ww + w / 2 + ws_2
1447
+ y1 = yy * hh + h + hs_2
1448
+ y2 = (yy + 1) * hh + hs_2
1449
+
1450
+ dc.drawLine( x1, y1, x1, y2 )
1451
+ }
1452
+ }
1453
+ end
1454
+
1455
+
1456
+
1457
+ #
1458
+ # Draw template of room squares in background
1459
+ #
1460
+ def draw_grid(dc, event)
1461
+
1462
+ dc.foreground = "black"
1463
+ dc.lineWidth = 0
1464
+ dc.lineStyle = LINE_ONOFF_DASH
1465
+
1466
+ ww = WW * @zoom
1467
+ hh = HH * @zoom
1468
+
1469
+ w = W * @zoom
1470
+ h = H * @zoom
1471
+
1472
+ ws_2 = WS_2 * @zoom
1473
+ hs_2 = HS_2 * @zoom
1474
+
1475
+ (0...@width).each { |xx|
1476
+ (0...@height).each { |yy|
1477
+ next if @pmap[xx][yy]
1478
+ x = xx * ww + ws_2
1479
+ y = yy * hh + hs_2
1480
+ dc.drawRectangle( x, y, w, h )
1481
+ }
1482
+ }
1483
+ end
1484
+
1485
+ #
1486
+ # Clean background to solid color
1487
+ #
1488
+ def draw_background(dc, event = nil)
1489
+ dc.foreground = @canvas.backColor
1490
+
1491
+ if event
1492
+ dc.fillRectangle(event.rect.x, event.rect.y, event.rect.w, event.rect.h)
1493
+ else
1494
+ dc.fillRectangle(0,0, @canvas.width, @canvas.height)
1495
+ end
1496
+ end
1497
+
1498
+ #
1499
+ # Draw connections among rooms
1500
+ #
1501
+ def draw_connections(dc)
1502
+ dc.lineStyle = LINE_SOLID
1503
+ dc.lineWidth = 3 * @zoom
1504
+ dc.lineWidth = 3 if dc.lineWidth < 3
1505
+ @pages[@page].connections.each { |c| c.draw(dc, @zoom, @options) }
1506
+ end
1507
+
1508
+ #
1509
+ # Draw rooms
1510
+ #
1511
+ def draw_rooms(dc)
1512
+ data = { }
1513
+ data['font'] = @font
1514
+ data['objfont'] = @objfont
1515
+ @pages[@page].rooms.each_with_index { |room, idx|
1516
+ room.draw(dc, @zoom, idx, @options, data)
1517
+ }
1518
+ end
1519
+
1520
+ #
1521
+ # Print map
1522
+ #
1523
+ def print(printer)
1524
+ end
1525
+
1526
+ #
1527
+ # Draw map
1528
+ #
1529
+ def draw(event = nil)
1530
+ pos = @scrollwindow.position
1531
+ w = @scrollwindow.getViewportWidth
1532
+ h = @scrollwindow.getViewportHeight
1533
+
1534
+ dc = FXDCWindow.new(@image)
1535
+ dc.setClipRectangle( -pos[0]-5, -pos[1]-5, w, h)
1536
+ dc.font = @font
1537
+ # dc.lineCap = CAP_ROUND
1538
+ draw_background(dc, event)
1539
+ draw_grid(dc, event) if @options['Grid Boxes']
1540
+ if @options['Grid Straight Connections']
1541
+ draw_straight_connections(dc, event)
1542
+ end
1543
+ if @options['Grid Diagonal Connections']
1544
+ draw_diagonal_connections(dc, event)
1545
+ end
1546
+ draw_connections(dc)
1547
+ draw_rooms(dc)
1548
+ dc.end
1549
+
1550
+
1551
+
1552
+ # Blit the off-screen image into canvas
1553
+ dc = FXDCWindow.new(@canvas)
1554
+ dc.setClipRectangle( -pos[0]-5, -pos[1]-5, w, h)
1555
+ dc.drawImage(@image,0,0)
1556
+ dc.end
1557
+ end
1558
+
1559
+
1560
+
1561
+ def _save
1562
+ if @complexConnection
1563
+ # If we have an incomplete connection, remove it
1564
+ @pages[@page].delete_connection_at(-1)
1565
+ end
1566
+
1567
+ if @filename !~ /\.map$/i
1568
+ @filename << '.map'
1569
+ end
1570
+
1571
+ status "Saving '#{@filename}'..."
1572
+ @version = FILE_FORMAT_VERSION
1573
+ begin
1574
+ f = File.open(@filename, "wb")
1575
+ f.puts Marshal.dump(self)
1576
+ f.close
1577
+ rescue => e
1578
+ status "Could not save '#{@filename}': #{e}"
1579
+ sleep 4
1580
+ return false
1581
+ end
1582
+ @modified = false
1583
+ status "Saved '#{@filename}'."
1584
+ sleep 0.5
1585
+ return true
1586
+ end
1587
+
1588
+ def save
1589
+ unless @filename
1590
+ save_as
1591
+ else
1592
+ _save
1593
+ end
1594
+ end
1595
+
1596
+ def save_as
1597
+ file = FXMapFileDialog.new(@window, "Save Map #{@name}").filename
1598
+ if file != ''
1599
+ @filename = file
1600
+ return _save
1601
+ end
1602
+ return false
1603
+ end
1604
+
1605
+
1606
+ def properties
1607
+ if not @@win
1608
+ @@win = FXMapDialogBox.new(@window)
1609
+ end
1610
+ @@win.copy_from(self)
1611
+ @@win.show
1612
+ end
1613
+
1614
+ end