ifmapper 0.5

Sign up to get free protection for your applications and to get access to all the features.
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