1addScript(name, tooltip, method) {
2    if (tooltip == null) tooltip = name;
3    menuItem = new JMenuItem(name);
4    menuItem.addActionListener(this);
5    menuItem.setToolTipText(tooltip);
6    global.scriptMenu.add(menuItem);
7    actionPerformed(ae) {
8        this.invokeMethod(method, null);
9    }
10}
11
12addScript("Randomize", "Randomize the selected wall polygons", "randomize");
13addScript("Shadow", "Adds a shadow to the selected wall polygons", "shadow");
14addScript("Round", "Rounds the corners of the selected wall polygons", "round");
15addScript("Image to polygon", "Creates a polygon from an image", "imageToPoly");
16addScript("Bevel-border", "Adds a bevel border to a polygon", "bevelBorder");
17addScript("Depth-decor", "Adds a depth decoration to a polygon", "depthDecor");
18addScript("Select all polygons", "Selects all top-level polygons", "selectAllPolys");
19
20import org.xpilot.jxpmap.*;
21
22
23/************************ Misc Utils ******************************/
24
25
26/**
27 * Utility to check if a map objects is a regular wall polygon
28 */
29isWall(p) {
30    return (p instanceof MapPolygon);
31}
32
33/**
34 * Finds a style with the matching id from the list
35 */
36findStyle(id, list) {
37    for (style:list)
38        if (style.id.equals(id))
39            return style;
40    return null;
41}
42
43/**
44 * Shows a message box.
45 */
46alert(msg) {
47    JOptionPane.showMessageDialog(null, msg);
48}
49
50/**
51 * Prompts the user for input.
52 */
53prompt(msg) {
54    return JOptionPane.showInputDialog(null, msg);
55}
56
57/**
58 * Prompts the user for input with the given default value.
59 */
60prompt(msg, def) {
61    return JOptionPane.showInputDialog(null, msg, def);
62}
63
64/**
65 * Gets the center point of the visible area on the map.
66 */
67center() {
68    center = new Point(editor.width / 2, editor.height / 2);
69    return editor.getInverse().transform(center, new Point());
70}
71
72/**
73 * Selects all top-level polygons.
74 */
75selectAllPolys() {
76    editor.setSelectedObject(null);
77    for(o:editor.model.objects)
78        if (o instanceof MapPolygon)
79            editor.addSelectedObject(o);
80    editor.repaint();
81}
82
83/************************ Randomize script ******************************/
84
85
86/**
87 * Randomizes a wall polygon with the given amount of pixels.
88 */
89doRandomize(pixels, o) {
90    if (isWall(o)) {
91        for (i = 0; i < o.getNumPoints(); i++) {
92            p = o.getPoint(i);
93            p.translate((int)((Math.random() - 0.5) * pixels * 64),
94            (int)((Math.random() - 0.5) * pixels * 64));
95            o.setPoint(i, p.x, p.y);
96        }
97    } else if (o instanceof Group) {
98        for (i:o.members) doRandomize(pixels, i);
99        o.invalidate();
100    }
101}
102
103randomize() {
104    if (editor.getSelectedObjects().isEmpty()) {
105        alert("First select a polygon");
106        return;
107    }
108    value = prompt("How many pixels to randomize");
109    if (value == null) return;
110    for (o:editor.selectedObjects)
111        doRandomize(Integer.parseInt(value), o);
112    editor.repaint();
113}
114
115
116/************************ Shadow script ******************************/
117
118
119/**
120 * Adds a drop shadow to a wall polygon by creating
121 * a dark gray decoration polygon below it.
122 */
123addShadow(polygon, style) {
124    p = polygon.getPolygon();
125    sp = new Polygon(p.xpoints, p.ypoints, p.npoints);
126    for(i = 0; i < sp.npoints; i++) {
127        sp.xpoints[i] += 240;
128        sp.ypoints[i] -= 640;
129    }
130    sp.invalidate();
131    decor = new Decoration();
132    decor.addToFront(new MapPolygon(sp, style));
133    editor.model.addToBack(decor);
134}
135
136/**
137 * Finds or creates the edge style and polygon style required
138 * for a drop shadow.
139 */
140getShadowStyle() {
141    model = editor.model;
142    es = findStyle("hidden", model.edgeStyles);
143    if (es == null) {
144        es = new LineStyle("hidden", 1, Color.darkGray, LineStyle.STYLE_HIDDEN);
145        model.edgeStyles.add(es);
146    }
147    ps = findStyle("shadow", model.polyStyles);
148    if (ps == null) {
149        ps = new PolygonStyle();
150        ps.setColor(new Color(50,50,50));
151        ps.setFillStyle(PolygonStyle.FILL_COLOR);
152        ps.setId("shadow");
153        ps.setVisibleInRadar(false);
154        ps.setDefaultEdgeStyle(es);
155        model.getPolyStyles().add(ps);
156    }
157    return ps;
158}
159
160doShadow(o) {
161    style = getShadowStyle();
162    if (isWall(o)) addShadow(o, style);
163    else if (o instanceof Group) {
164        for (i:o.members) doShadow(i);
165        o.invalidate();
166    }
167}
168
169shadow() {
170    if (editor.selectedObjects.isEmpty()) {
171        alert("First select a polygon");
172        return;
173    }
174    for (o:editor.selectedObjects) doShadow(o);
175    editor.repaint();
176}
177
178
179/************************ Round script ******************************/
180
181/**
182 * Moves a point to the specified direction the specified length.
183 */
184movePoint(p, dir, len) {
185    a = len * Math.pow(p.distanceSq(dir), -0.5);
186    p.translate((int)(a * (dir.x - p.x)), (int)(a * (dir.y - p.y)));
187}
188
189/**
190 * Rounds the corners of a wall polygon by adding one additional
191 * vertex to each corner and moving the original vertex. The max parameter
192 * controls the maximum amount of pixels a vertex gets moved.
193 */
194doRound(mp, max) {
195    if (!isWall(mp)) return;
196    if (mp instanceof Group) {
197        for (o:mp.members)
198            doRound(o, max);
199        mp.invalidate();
200    }
201    if (mp.getNumPoints() < 3) return;
202    minDist = 1 * 64;
203    maxMove = max * 64;
204    p2 = mp.getPoint(mp.getNumPoints() - 1);
205    p3 = mp.getPoint(0);
206    for (i = 1; i <= mp.getNumPoints(); i++) {
207        p1 = p2;
208        p2 = p3;
209        p3 = mp.getPoint(i % mp.getNumPoints());
210        d12 = p1.distance(p2);
211        if (d12 <= minDist) continue;
212        d23 = p2.distance(p3);
213        if (d23 <= minDist) continue;
214        pn = new Point(p2);
215        movePoint(pn, p1, Math.min(maxMove, (int)(d12 / 2)));
216        movePoint(p2, p3, Math.min(maxMove, (int)(d23 / 2)));
217        mp.insertPoint(i - 1, pn);
218        mp.setPoint(i, p2.x, p2.y);
219        i++;
220    }
221}
222
223round() {
224    if (editor.selectedObjects.isEmpty()) {
225        alert("First select a polygon");
226        return;
227    }
228    value = prompt("How many pixels to round");
229    if (value == null) return;
230    max = Integer.parseInt(value);
231    value = prompt("How many times to run");
232    if (value == null) return;
233    times = Integer.parseInt(value);
234    for (o:editor.selectedObjects)
235        for (i = 0; i < times; i++)
236            doRound(o, max);
237    editor.repaint();
238}
239
240/*********************** Image to polygon ***************************/
241
242imageToPoly(pixmap) {
243    model = editor.model;
244    poly = FindPolygon.findPolygon(pixmap.image);
245    if (poly == null) {
246        alert("Couldn't find a polygon in the image");
247        return;
248    }
249    es = findStyle("hidden", model.edgeStyles);
250    if (es == null) {
251        es = new LineStyle("hidden", 1, Color.darkGray, LineStyle.STYLE_HIDDEN);
252        model.edgeStyles.add(es);
253    }
254    ps = findStyle(pixmap.fileName, model.polyStyles);
255    if (ps == null) {
256        ps = new PolygonStyle();
257        ps.setId(pixmap.fileName);
258        ps.setColor(new Color(0,0,128));
259        ps.setFillStyle(PolygonStyle.FILL_TEXTURED);
260        ps.setTexture(pixmap);
261        ps.setVisibleInRadar(true);
262        ps.setDefaultEdgeStyle(es);
263        model.getPolyStyles().add(ps);
264    }
265    p = center();
266    poly.translate(p.x, p.y);
267    editor.addMapObject(new MapPolygon(poly, ps));
268}
269
270imageToPoly() {
271    model = editor.model;
272    if (model.pixmaps.isEmpty()) {
273        alert("First add an image using the Image Manager");
274        return;
275    }
276    map = new HashMap();
277    for(p:editor.model.pixmaps)
278        map.put(p.fileName, p);
279    images = map.keySet().toArray();
280    name = JOptionPane.showInputDialog(null,
281        "Select an image", "Question",
282        JOptionPane.INFORMATION_MESSAGE, null,
283        images, images[0]);
284    pixmap = map.get(name);
285    if (pixmap == null) return;
286    imageToPoly(pixmap);
287}
288
289/*********************** 3D effects ***************************/
290
291import java.awt.image.*;
292
293/**
294 * A 2D vector object.
295 */
296vec(x, y) {
297    add(v) { return vec(v.x + x, v.y + y); }
298    mul(a) { return vec(a*x, a*y); }
299    len() { return Math.sqrt(1.0*x*x + 1.0*y*y); }
300    unit() { return mul(1d/len()); }
301    normal() { return vec(-y, x); }
302    dot(v) { return x*v.x + y*v.y; }
303    eq(v) { return v.x == x && v.y == y; }
304    return this;
305}
306
307/**
308 * Gets the average color of the given polygon style.
309 */
310getAverageColor(style) {
311    averageColor(img) {
312        bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
313        bi.graphics.drawImage(img, 0, 0, 1, 1, Color.black, null);
314        return new Color(bi.getRGB(0, 0));
315    }
316    switch(style.fillStyle) {
317        case PolygonStyle.FILL_COLOR:
318            if (!Color.black.equals(style.color))
319                return style.color;
320            break;
321        case PolygonStyle.FILL_TEXTURED:
322            return averageColor(style.texture.image);
323    }
324    return Color.darkGray;
325}
326
327/**
328 * Returns an image that is a copy of the given image img
329 * with brightness adjusted by the given level.
330 *
331 * @param img the original image
332 * @param level a float that is used to multiply the color components
333 * of the original image
334 */
335adjustBrightness(img, level) {
336    op = new ConvolveOp(new Kernel(1, 1, new float[] { level }));
337    dst = op.createCompatibleDestImage(img,
338        new DirectColorModel(24, 0xff0000, 0xff00, 0xff));
339    op.filter(img, dst);
340    return dst;
341}
342
343
344getColoredStyle(level, name, orig) {
345    var id = level < -0.5 ? 0 : level < 0 ? 1 : level < 0.5 ? 2 : 3;
346    var sid = name + "-color" + id;
347    var ps = findStyle(sid, editor.model.polyStyles);
348    if (ps == null) {
349        var es = findStyle("hidden", editor.model.edgeStyles);
350        if (es == null) {
351            es = new LineStyle("hidden", 1, Color.darkGray,
352                LineStyle.STYLE_HIDDEN);
353            editor.model.edgeStyles.add(es);
354        }
355        var c = null;
356        switch(id) {
357            case 0:  c = orig.brighter().brighter(); break;
358            case 1:  c = orig.brighter(); break;
359            case 2:  c = orig.darker(); break;
360            default: c = orig.darker().darker(); break;
361        }
362        ps = new PolygonStyle();
363        ps.setId(sid);
364        ps.setColor(c);
365        ps.setFillStyle(PolygonStyle.FILL_COLOR);
366        ps.setVisibleInRadar(false);
367        ps.setDefaultEdgeStyle(es);
368        editor.model.polyStyles.add(ps);
369    }
370    return ps;
371}
372
373getTexturedStyle(level, orig) {
374    var id = level < -0.5 ? 0 : level < 0 ? 1 : level < 0.5 ? 2 : 3;
375    var sid = orig.id + "-light" + id;
376    var ps = findStyle(sid, editor.model.polyStyles);
377    if (ps == null) {
378        var es = findStyle("hidden", editor.model.edgeStyles);
379        if (es == null) {
380            es = new LineStyle("hidden", 1, Color.darkGray,
381                LineStyle.STYLE_HIDDEN);
382            editor.model.edgeStyles.add(es);
383        }
384        var c = orig.color;
385        if (c == null) c = Color.black;
386        switch(id) {
387            case 0:  c = c.brighter().brighter(); break;
388            case 1:  c = c.brighter(); break;
389            case 2:  c = c.darker(); break;
390            default: c = c.darker().darker(); break;
391        }
392        var img = adjustBrightness(orig.texture.image, -level + 1.2);
393        var pixmap = new Pixmap();
394        pixmap.image = img;
395        pixmap.fileName = "light" + id + "-" + orig.texture.fileName;
396        editor.model.pixmaps.add(pixmap);
397        ps = new PolygonStyle();
398        ps.setId(sid);
399        ps.setColor(c);
400        ps.setTexture(pixmap);
401        ps.setFillStyle(PolygonStyle.FILL_TEXTURED);
402        ps.setVisibleInRadar(false);
403        ps.setDefaultEdgeStyle(es);
404        editor.model.polyStyles.add(ps);
405    }
406    return ps;
407}
408
409/**
410 * Returns equivalent polygon without edges with length 0.
411 */
412filterPoints(poly) {
413    var pnew = new Polygon();
414    int x = poly.xpoints[0];
415    int y = poly.ypoints[0];
416    pnew.addPoint(x, y);
417    for(int i = 1; i < poly.npoints; i++) {
418        int x2 = poly.xpoints[i];
419        int y2 = poly.ypoints[i];
420        if (x == x2 && y == y2) continue;
421        pnew.addPoint(x2, y2);
422        x = x2; y = y2;
423    }
424    return pnew;
425}
426
427/**
428 * Returns a group of polygons that makes the given map polygon look like
429 * it had 3rd dimension (depth).
430 *
431 * @param mp the map polygon
432 * @param d the depth in pixels
433 * @param view a vec that shows the view direction
434 * @param light a vec that shows the light direction
435 * @param tex if true then the added polygons are filled with textures
436 * otherwise they are filled with colors
437 * @param decor if true then the group is a Decoration otherwise it is a
438 * Group
439 */
440makeDepthBorder(mp, d, view, light, tex, decor) {
441    var vu = view.unit();
442    var lu = light.unit();
443    int sgn = mp.isCounterClockwise() ? -1 : 1;
444    var dv = view.mul(-d);
445    var quads = new ArrayList();
446    var p = filterPoints(mp.polygon);
447    int np = p.npoints;
448    if (np < 2) return;
449    if (mp.defaultStyle.fillStyle != PolygonStyle.FILL_TEXTURED)
450        tex = false;
451    var orig = tex ? mp.defaultStyle : getAverageColor(mp.defaultStyle);
452    var p1 = vec(p.xpoints[0], p.ypoints[0]);
453    for(int i = 0; i < np; i++) {
454        var p2 = vec(p.xpoints[(i + 1) % np], p.ypoints[(i + 1) % np]);
455        var n = p2.add(p1.mul(-1)).mul(sgn).normal().unit();
456        if (n.dot(view) <= 0) {
457            var level = n.dot(lu);
458            var style = tex ? getTexturedStyle(level, orig)
459                : getColoredStyle(level, mp.defaultStyle.id, orig);
460            quads.add(new MapPolygon(new Polygon(
461                new int[] {p1.x, (int)(p1.x + dv.x), (int)(p2.x + dv.x), p2.x},
462                new int[] {p1.y, (int)(p1.y + dv.y), (int)(p2.y + dv.y), p2.y},
463                4), style));
464        }
465        p1 = p2;
466    }
467    if (decor) return new Decoration(quads);
468    return new Group(quads);
469}
470
471makeBevelBorder(mp, d, light, tex) {
472    if (mp.defaultStyle.fillStyle != PolygonStyle.FILL_TEXTURED)
473        tex = false;
474    var lu = light.unit();
475    int sgn = mp.isCounterClockwise() ? 1 : -1;
476    var quads = new ArrayList();
477    var p = filterPoints(mp.polygon);
478    var np = p.npoints;
479    if (np < 3) return;
480    if (mp.defaultStyle.fillStyle != PolygonStyle.FILL_TEXTURED)
481        tex = false;
482    var orig = tex ? mp.defaultStyle : getAverageColor(mp.defaultStyle);
483    var p1 = vec(p.xpoints[0], p.ypoints[0]);
484    var p2 = vec(p.xpoints[1], p.ypoints[1]);
485    var p3 = vec(p.xpoints[2], p.ypoints[2]);
486    for(int i = 0; i < np; i++) {
487        var p4 = vec(p.xpoints[(i + 3) % np], p.ypoints[(i + 3) % np]);
488        var n12 = p2.add(p1.mul(-1)).mul(sgn).normal().unit();
489        var n23 = p3.add(p2.mul(-1)).mul(sgn).normal().unit();
490        var n34 = p4.add(p3.mul(-1)).mul(sgn).normal().unit();
491        var newp2 = p2.add(n12.add(n23).unit().mul(d));
492        var newp3 = p3.add(n23.add(n34).unit().mul(d));
493        var level = -n23.dot(lu);
494        var style = tex ? getTexturedStyle(level, orig)
495            : getColoredStyle(level, mp.defaultStyle.id, orig);
496        quads.add(new MapPolygon(new Polygon(
497            new int[] {p2.x, p3.x, newp3.x, newp2.x},
498            new int[] {p2.y, p3.y, newp3.y, newp2.y},
499            4), style));
500        p1 = p2; p2 = p3; p3 = p4;
501    }
502    return new Group(quads);
503}
504
505bevelBorder() {
506    if (editor.selectedObjects.isEmpty()) {
507        alert("First select a polygon");
508        return;
509    }
510    var value = prompt("Enter border height in pixels", "10");
511    if (value == null) return;
512    var height = Integer.parseInt(value) * 64;
513    for(o:editor.selectedObjects) {
514        if (isWall(o)) {
515            var bb = makeBevelBorder(o, height, vec(1,-1), true);
516            editor.addMapObject(bb);
517        }
518    }
519    editor.repaint();
520}
521
522depthDecor() {
523    if (editor.selectedObjects.isEmpty()) {
524        alert("First select a polygon");
525        return;
526    }
527    var value = prompt("Enter depth in pixels", "10");
528    if (value == null) return;
529    var height = Integer.parseInt(value) * 64;
530    for(o:editor.selectedObjects) {
531        if (isWall(o)) {
532            var db = makeDepthBorder(o, height,
533                vec(-1,2), vec(1,-1), true, false);
534            editor.model.addToBack(db);
535        }
536    }
537    editor.repaint();
538}
539
540
541
542/************************ User scripts ******************************/
543
544
545loadUserScripts() {
546    String home = System.getProperty("user.home");
547    f = new File(home + "/.jxpmaprc");
548    if (!f.exists()) f = new File(home + "/jxpmaprc.bsh");
549    if (f.exists()) source(f.getAbsolutePath());
550}
551loadUserScripts();
552