diff --git a/.gitignore b/.gitignore index f112f7f..fb18345 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ bin-release/ # Other files and folders .settings/ +*.swf # Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties` # should NOT be excluded as they contain compiler settings and other important diff --git a/editor/.actionScriptProperties b/editor/.actionScriptProperties new file mode 100644 index 0000000..afc7408 --- /dev/null +++ b/editor/.actionScriptProperties @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/editor/.flexProperties b/editor/.flexProperties new file mode 100644 index 0000000..bd53f3a --- /dev/null +++ b/editor/.flexProperties @@ -0,0 +1,3 @@ + + + diff --git a/editor/.project b/editor/.project new file mode 100644 index 0000000..b0b7fab --- /dev/null +++ b/editor/.project @@ -0,0 +1,24 @@ + + + MapShapeGen + + + + + + com.adobe.flexbuilder.project.flexbuilder + + + + + com.adobe.flexbuilder.project.apollobuilder + + + + + + com.adobe.flexbuilder.project.apollonature + com.adobe.flexbuilder.project.flexnature + com.adobe.flexbuilder.project.actionscriptnature + + diff --git a/editor/assets/fonts/Ubuntu/LICENCE.txt b/editor/assets/fonts/Ubuntu/LICENCE.txt new file mode 100644 index 0000000..ae78a8f --- /dev/null +++ b/editor/assets/fonts/Ubuntu/LICENCE.txt @@ -0,0 +1,96 @@ +------------------------------- +UBUNTU FONT LICENCE Version 1.0 +------------------------------- + +PREAMBLE +This licence allows the licensed fonts to be used, studied, modified and +redistributed freely. The fonts, including any derivative works, can be +bundled, embedded, and redistributed provided the terms of this licence +are met. The fonts and derivatives, however, cannot be released under +any other licence. The requirement for fonts to remain under this +licence does not require any document created using the fonts or their +derivatives to be published under this licence, as long as the primary +purpose of the document is not to be a vehicle for the distribution of +the fonts. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this licence and clearly marked as such. This may +include source files, build scripts and documentation. + +"Original Version" refers to the collection of Font Software components +as received under this licence. + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to +a new environment. + +"Copyright Holder(s)" refers to all individuals and companies who have a +copyright ownership of the Font Software. + +"Substantially Changed" refers to Modified Versions which can be easily +identified as dissimilar to the Font Software by users of the Font +Software comparing the Original Version with the Modified Version. + +To "Propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification and with or without charging +a redistribution fee), making available to the public, and in some +countries other activities as well. + +PERMISSION & CONDITIONS +This licence does not grant any rights under trademark law and all such +rights are reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to propagate the Font Software, subject to +the below conditions: + +1) Each copy of the Font Software must contain the above copyright +notice and this licence. These can be included either as stand-alone +text files, human-readable headers or in the appropriate machine- +readable metadata fields within text or binary files as long as those +fields can be easily viewed by the user. + +2) The font name complies with the following: +(a) The Original Version must retain its name, unmodified. +(b) Modified Versions which are Substantially Changed must be renamed to +avoid use of the name of the Original Version or similar names entirely. +(c) Modified Versions which are not Substantially Changed must be +renamed to both (i) retain the name of the Original Version and (ii) add +additional naming elements to distinguish the Modified Version from the +Original Version. The name of such Modified Versions must be the name of +the Original Version, with "derivative X" where X represents the name of +the new work, appended to that name. + +3) The name(s) of the Copyright Holder(s) and any contributor to the +Font Software shall not be used to promote, endorse or advertise any +Modified Version, except (i) as required by this licence, (ii) to +acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with +their explicit written permission. + +4) The Font Software, modified or unmodified, in part or in whole, must +be distributed entirely under this licence, and must not be distributed +under any other licence. The requirement for fonts to remain under this +licence does not affect any document created using the Font Software, +except any version of the Font Software extracted from a document +created using the Font Software may only be distributed under this +licence. + +TERMINATION +This licence becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. diff --git a/editor/assets/fonts/Ubuntu/Ubuntu-B.ttf b/editor/assets/fonts/Ubuntu/Ubuntu-B.ttf new file mode 100644 index 0000000..c0142fe Binary files /dev/null and b/editor/assets/fonts/Ubuntu/Ubuntu-B.ttf differ diff --git a/editor/assets/fonts/Ubuntu/Ubuntu-BI.ttf b/editor/assets/fonts/Ubuntu/Ubuntu-BI.ttf new file mode 100644 index 0000000..12e4c7d Binary files /dev/null and b/editor/assets/fonts/Ubuntu/Ubuntu-BI.ttf differ diff --git a/editor/assets/fonts/Ubuntu/Ubuntu-C.ttf b/editor/assets/fonts/Ubuntu/Ubuntu-C.ttf new file mode 100644 index 0000000..8d3e867 Binary files /dev/null and b/editor/assets/fonts/Ubuntu/Ubuntu-C.ttf differ diff --git a/editor/assets/fonts/Ubuntu/Ubuntu-L.ttf b/editor/assets/fonts/Ubuntu/Ubuntu-L.ttf new file mode 100644 index 0000000..7b7ac7d Binary files /dev/null and b/editor/assets/fonts/Ubuntu/Ubuntu-L.ttf differ diff --git a/editor/assets/fonts/Ubuntu/Ubuntu-LI.ttf b/editor/assets/fonts/Ubuntu/Ubuntu-LI.ttf new file mode 100644 index 0000000..e36de45 Binary files /dev/null and b/editor/assets/fonts/Ubuntu/Ubuntu-LI.ttf differ diff --git a/editor/assets/fonts/Ubuntu/Ubuntu-M.ttf b/editor/assets/fonts/Ubuntu/Ubuntu-M.ttf new file mode 100644 index 0000000..443ec8b Binary files /dev/null and b/editor/assets/fonts/Ubuntu/Ubuntu-M.ttf differ diff --git a/editor/assets/fonts/Ubuntu/Ubuntu-MI.ttf b/editor/assets/fonts/Ubuntu/Ubuntu-MI.ttf new file mode 100644 index 0000000..321eccf Binary files /dev/null and b/editor/assets/fonts/Ubuntu/Ubuntu-MI.ttf differ diff --git a/editor/assets/fonts/Ubuntu/Ubuntu-R.ttf b/editor/assets/fonts/Ubuntu/Ubuntu-R.ttf new file mode 100644 index 0000000..45a038b Binary files /dev/null and b/editor/assets/fonts/Ubuntu/Ubuntu-R.ttf differ diff --git a/editor/assets/fonts/Ubuntu/Ubuntu-RI.ttf b/editor/assets/fonts/Ubuntu/Ubuntu-RI.ttf new file mode 100644 index 0000000..6f819f6 Binary files /dev/null and b/editor/assets/fonts/Ubuntu/Ubuntu-RI.ttf differ diff --git a/editor/assets/fonts/Ubuntu/UbuntuMono-B.ttf b/editor/assets/fonts/Ubuntu/UbuntuMono-B.ttf new file mode 100644 index 0000000..7bd6665 Binary files /dev/null and b/editor/assets/fonts/Ubuntu/UbuntuMono-B.ttf differ diff --git a/editor/assets/fonts/Ubuntu/UbuntuMono-BI.ttf b/editor/assets/fonts/Ubuntu/UbuntuMono-BI.ttf new file mode 100644 index 0000000..6c5b8ba Binary files /dev/null and b/editor/assets/fonts/Ubuntu/UbuntuMono-BI.ttf differ diff --git a/editor/assets/fonts/Ubuntu/UbuntuMono-R.ttf b/editor/assets/fonts/Ubuntu/UbuntuMono-R.ttf new file mode 100644 index 0000000..fdd309d Binary files /dev/null and b/editor/assets/fonts/Ubuntu/UbuntuMono-R.ttf differ diff --git a/editor/assets/fonts/Ubuntu/UbuntuMono-RI.ttf b/editor/assets/fonts/Ubuntu/UbuntuMono-RI.ttf new file mode 100644 index 0000000..18f81a2 Binary files /dev/null and b/editor/assets/fonts/Ubuntu/UbuntuMono-RI.ttf differ diff --git a/editor/libs/mignari.swc b/editor/libs/mignari.swc new file mode 100644 index 0000000..77c7de0 Binary files /dev/null and b/editor/libs/mignari.swc differ diff --git a/editor/libs/mignari_core.swc b/editor/libs/mignari_core.swc new file mode 100644 index 0000000..c1ccbf5 Binary files /dev/null and b/editor/libs/mignari_core.swc differ diff --git a/editor/libs/otlib_core.swc b/editor/libs/otlib_core.swc new file mode 100644 index 0000000..2f902de Binary files /dev/null and b/editor/libs/otlib_core.swc differ diff --git a/editor/locale/en_US/mapgen_strings.properties b/editor/locale/en_US/mapgen_strings.properties new file mode 100644 index 0000000..af926bb --- /dev/null +++ b/editor/locale/en_US/mapgen_strings.properties @@ -0,0 +1,22 @@ +# General +OPTIONS=Options +SELECT_OUTPUT_FOLDER=Select the output folder + +# Menu +MENU_FILE=File +MENU_FILE_NEW=New +MENU_FILE_OPEN=Open... +MENU_FILE_GENERATE=Generate +MENU_FILE_SAVE=Save +MENU_FILE_PREFERENCES=Preferences +MENU_FILE_EXIT=Exit +MENU_HELP=Help +MENU_HELP_ABOUT=About + +# Toolbar + +# PreferencesWindow +PW_TITLE=Preferences + +# AboutWindow +ABOUT_WINDOW_TITLE=About \ No newline at end of file diff --git a/editor/src/MapShapeGen-app.xml b/editor/src/MapShapeGen-app.xml new file mode 100644 index 0000000..553967b --- /dev/null +++ b/editor/src/MapShapeGen-app.xml @@ -0,0 +1,46 @@ + + + + com.mignari.MapShapeGen + MapShapeGen + MapShapeGen + 1.0.0 + 1.0 + + + [This value will be overwritten by Flash Builder in the output app.xml] + false + false + false + + + en + + + icon/app/icon16.png + icon/app/icon32.png + icon/app/icon36.png + icon/app/icon48.png + icon/app/icon72.png + icon/app/icon96.png + icon/app/icon114.png + icon/app/icon128.png + icon/app/icon144.png + icon/app/icon152.png + + + + + Shape + otshape + MapShapeGen File + binary + + icon/system/icon16.png + icon/system/icon32.png + icon/system/icon48.png + icon/system/icon128.png + + + + diff --git a/editor/src/MapShapeGen.css b/editor/src/MapShapeGen.css new file mode 100644 index 0000000..3e7f210 --- /dev/null +++ b/editor/src/MapShapeGen.css @@ -0,0 +1,46 @@ +@namespace s "library://ns.adobe.com/flex/spark"; +@namespace mx "library://ns.adobe.com/flex/mx"; + +@font-face +{ + src: url("../assets/fonts/Ubuntu/Ubuntu-L.ttf"); + fontFamily: ubuntu; + advancedAntiAliasing: true; + fontStyle: normal; + embedAsCFF:true; +} + +@font-face +{ + src: url("../assets/fonts/Ubuntu/Ubuntu-B.ttf"); + fontFamily: ubuntu; + advancedAntiAliasing: true; + fontStyle: normal; + fontWeight: bold; + embedAsCFF:true; +} + +@font-face +{ + src: url("../assets/fonts/Ubuntu/Ubuntu-LI.ttf"); + fontFamily: ubuntu; + advancedAntiAliasing: true; + fontStyle: italic; + fontWeight: normal; + embedAsCFF:true; +} + +global +{ + chromeColor: #494949; + rollOverColor: #355D89; + selectionColor: #294867; + color: #DFDFDF; + contentBackgroundColor: #494949; + focusedTextSelectionColor: #272727; + symbolColor: #DFDFDF; + fontFamily: ubuntu; + modal-transparency-blur: 0; + modal-transparency: 0.3; + modalTransparencyColor: #656565; +} diff --git a/editor/src/MapShapeGen.mxml b/editor/src/MapShapeGen.mxml new file mode 100644 index 0000000..225e404 --- /dev/null +++ b/editor/src/MapShapeGen.mxml @@ -0,0 +1,543 @@ + + + + + + + + 0) + { + if (m_creationComplete) + { + setInvokeArguments(event.arguments); + } + else + { + m_invokeArguments = event.arguments; + } + } + } + + protected function applicationPreinitializeHandler(event:FlexEvent):void + { + m_progressWidget = new ProgressWidget(); + + m_communicator = new WorkerCommunicator(Workers.MapShapeGenWorker); + m_communicator.registerClass(MapShapeGenSettings); + m_communicator.registerClass(MapInfo); + m_communicator.registerCallback(LogCommand, logCallback); + m_communicator.registerCallback(MapInfoCommand, mapInfoCallback); + m_communicator.registerCallback(UpdatePreviewCommand, updatePreviewCallback); + m_communicator.registerCallback(ProgressCommand, progressCallback); + m_communicator.start(); + + m_sharedByteArray = new ByteArray(); + m_sharedByteArray.shareable = true; + m_communicator.worker.setSharedProperty("sharedByteArray", m_sharedByteArray); + } + + protected function applicationCreationCompleteHandler(event:FlexEvent):void + { + Alert.okLabel = "Ok"; + + loadSettings(); + + m_canvas = new Canvas(1024, 1024); + image.source = m_canvas.bitmap; + + m_creationComplete = true; + + if (m_isFirstRun) + { + firstRun(); + } + } + + protected function applicationClosingHandler(event:Event):void + { + this.saveSettings(); + } + + protected function nativeMenuSelectedHandler(event:MenuEvent):void + { + switch(event.data) + { + case Menu.FILE_NEW: + this.newShape(); + break; + + case Menu.FILE_OPEN: + this.openShape(); + break; + + case Menu.FILE_GENERATE: + this.generateShape(); + break; + + case Menu.FILE_SAVE: + this.saveMap(); + break; + + case Menu.FILE_PREFERENCES: + this.openPreferences(); + break; + + case Menu.FILE_EXIT: + this.close(); + break; + + case Menu.HELP_ABOUT: + this.openAboutWindow(); + break; + } + } + + protected function nativeMenuShowMenuItem(event:FlexNativeMenuEvent):void + { + var index:uint = CapabilitiesUtil.isMacOS ? 1 : 0; + var items:Array = nativeMenu.items; + + // File > New + items[index].submenu.items[0].enabled = !this.mapSaving; + + // File > Open + items[index].submenu.items[1].enabled = !this.mapSaving; + + // File > Generate + items[index].submenu.items[2].enabled = !this.mapSaving; + + // File > Save + items[index].submenu.items[3].enabled = this.mapCreated && !this.mapSaving; + + // File > Preferences + items[index].submenu.items[5].enabled = !this.mapSaving; + } + + protected function islandTypeChangeHandler(event:IndexChangeEvent):void + { + m_settings.islandType = IslandType.value(event.newIndex); + m_communicator.sendCommand(new SettingsCommand(m_settings)); + updateShape(); + } + + protected function pointTypeChangeHandler(event:IndexChangeEvent):void + { + m_settings.pointType = PointType.value(event.newIndex); + m_communicator.sendCommand(new SettingsCommand(m_settings)); + updateShape(); + } + + protected function beachCheckBoxChangeHandler(event:Event):void + { + m_settings.showBeach = beachCheckBox.selected; + m_communicator.sendCommand(new SettingsCommand(m_settings)); + updateShape(); + } + + protected function riversCheckBoxChangeHandler(event:Event):void + { + m_settings.showRivers = riversCheckBox.selected; + m_communicator.sendCommand(new SettingsCommand(m_settings)); + updateShape(); + } + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Perlin + Radial + + + + + + + + + + + Relaxed + Random + Square + Hexagon + + + + + + + + + + + + + + + + + + diff --git a/editor/src/MapShapeGenWorker.as b/editor/src/MapShapeGenWorker.as new file mode 100644 index 0000000..9cdee4d --- /dev/null +++ b/editor/src/MapShapeGenWorker.as @@ -0,0 +1,194 @@ +/* +* Copyright (c) 2016 Nailson S. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +package +{ + import com.mapgen.Canvas; + import com.mapgen.Generator; + import com.mapgen.commands.CreateShapeCommand; + import com.mapgen.commands.MapInfoCommand; + import com.mapgen.commands.OpenShapeCommand; + import com.mapgen.commands.ProgressCommand; + import com.mapgen.commands.SaveMapCommand; + import com.mapgen.commands.SettingsCommand; + import com.mapgen.commands.UpdatePreviewCommand; + import com.mapgen.commands.UpdateShapeComamnd; + import com.mapgen.settings.MapShapeGenSettings; + import com.mapgen.utils.MapInfo; + import com.mignari.errors.NullArgumentError; + import com.mignari.errors.NullOrEmptyArgumentError; + import com.mignari.utils.isNullOrEmpty; + import com.mignari.workers.IWorkerCommunicator; + import com.mignari.workers.WorkerCommunicator; + + import flash.display.Sprite; + import flash.filesystem.File; + import flash.utils.ByteArray; + + public class MapShapeGenWorker extends Sprite + { + //-------------------------------------------------------------------------- + // PROPERTIES + //-------------------------------------------------------------------------- + + private var m_communicator:IWorkerCommunicator; + private var m_generator:Generator; + private var m_sharedByteArray:ByteArray; + private var m_canvas:Canvas; + private var m_settings:MapShapeGenSettings; + + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function MapShapeGenWorker() + { + m_communicator = new WorkerCommunicator(); + m_communicator.registerClass(MapShapeGenSettings); + m_communicator.registerClass(MapInfo); + m_communicator.registerCallback(SettingsCommand, settingsCallback); + m_communicator.registerCallback(CreateShapeCommand, createShapeCallback); + m_communicator.registerCallback(OpenShapeCommand, openShapeCallback); + m_communicator.registerCallback(UpdateShapeComamnd, updateShapeCallback); + m_communicator.registerCallback(SaveMapCommand, saveMapCallback); + m_communicator.start(); + + m_sharedByteArray = m_communicator.worker.getSharedProperty("sharedByteArray"); + m_canvas = new Canvas(1024, 1024); + m_generator = new Generator(); + m_generator.onChanged.add(changedCallback); + m_generator.onProgress.add(progressCallback); + } + + //-------------------------------------------------------------------------- + // METHODS + //-------------------------------------------------------------------------- + + //-------------------------------------- + // Private + //-------------------------------------- + + private function sendMapInfo():void + { + var info:MapInfo = new MapInfo(); + info.name = m_generator.name; + info.width = m_generator.width; + info.height = m_generator.height; + info.seed = m_generator.islandSeed; + info.islandType = m_generator.islandType; + info.pointType = m_generator.pointType; + info.beach = m_generator.beach; + info.rivers = m_generator.rivers; + info.created = m_generator.created; + info.changed = m_generator.changed; + info.saving = m_generator.saving; + m_communicator.sendCommand(new MapInfoCommand(info)); + } + + private function sendPreview():void + { + if (m_generator.created) + { + m_canvas.clear(); + m_canvas.draw(m_generator); + m_canvas.copyToByteArray(m_sharedByteArray); + m_communicator.sendCommand(new UpdatePreviewCommand(m_generator.islandSeed)); + } + } + + private function changedCallback():void + { + this.sendMapInfo(); + this.sendPreview(); + } + + private function progressCallback(id:String, value:uint, total:uint, label:String):void + { + m_communicator.sendCommand(new ProgressCommand(id, value, total, label)); + } + + private function settingsCallback(settings:MapShapeGenSettings):void + { + if (!settings) + { + throw new NullArgumentError("settings"); + } + + m_settings = settings; + } + + private function createShapeCallback(name:String, width:uint, height:uint):void + { + if (isNullOrEmpty(name)) + { + throw new NullOrEmptyArgumentError("name"); + } + + var seed:String = Generator.generateSeed(); + m_generator.create(name + "_" + seed, + width, + height, + seed, + m_settings.islandType, + m_settings.pointType, + m_settings.showBeach, + m_settings.showRivers); + } + + private function openShapeCallback(file:File):void + { + m_generator.load(file); + } + + private function updateShapeCallback(islandType:String, pointType:String, beach:Boolean, rivers:Boolean):void + { + if (isNullOrEmpty(islandType)) + { + throw new NullOrEmptyArgumentError("islandType"); + } + + if (isNullOrEmpty(pointType)) + { + throw new NullOrEmptyArgumentError("pointType"); + } + + m_generator.create(m_generator.name, + m_generator.width, + m_generator.height, + m_generator.islandSeed, + islandType, + pointType, + beach, + rivers); + } + + private function saveMapCallback():void + { + m_generator.save(m_settings.ouputDirectory, + m_settings.waterItem, + m_settings.sandItem, + m_settings.grassItem, + m_settings.savePNG, + m_settings.version); + } + } +} diff --git a/editor/src/Workers.as b/editor/src/Workers.as new file mode 100644 index 0000000..cbacab5 --- /dev/null +++ b/editor/src/Workers.as @@ -0,0 +1,14 @@ +package +{ + import flash.utils.ByteArray; + + public class Workers + { + [Embed(source="../workerswfs/MapShapeGenWorker.swf", mimeType="application/octet-stream")] + private static var MapShapeGenWorkerByteClass:Class; + public static function get MapShapeGenWorker():ByteArray + { + return new MapShapeGenWorkerByteClass(); + } + } +} diff --git a/editor/src/com/mapgen/commands/CreateShapeCommand.as b/editor/src/com/mapgen/commands/CreateShapeCommand.as new file mode 100644 index 0000000..587e881 --- /dev/null +++ b/editor/src/com/mapgen/commands/CreateShapeCommand.as @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2015 Nailson S. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +package com.mapgen.commands +{ + import com.mignari.workers.WorkerCommand; + + public class CreateShapeCommand extends WorkerCommand + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function CreateShapeCommand(name:String, width:uint, height:uint) + { + super(name, width, height); + } + } +} diff --git a/editor/src/com/mapgen/commands/MapInfoCommand.as b/editor/src/com/mapgen/commands/MapInfoCommand.as new file mode 100644 index 0000000..0cafc9c --- /dev/null +++ b/editor/src/com/mapgen/commands/MapInfoCommand.as @@ -0,0 +1,39 @@ +/* +* Copyright (c) 2015 Nailson S. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +package com.mapgen.commands +{ + import com.mapgen.utils.MapInfo; + import com.mignari.workers.WorkerCommand; + + public class MapInfoCommand extends WorkerCommand + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function MapInfoCommand(info:MapInfo) + { + super(info); + } + } +} diff --git a/editor/src/com/mapgen/commands/OpenShapeCommand.as b/editor/src/com/mapgen/commands/OpenShapeCommand.as new file mode 100644 index 0000000..585670a --- /dev/null +++ b/editor/src/com/mapgen/commands/OpenShapeCommand.as @@ -0,0 +1,40 @@ +/* +* Copyright (c) 2015 Nailson S. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +package com.mapgen.commands +{ + import com.mignari.workers.WorkerCommand; + + import flash.filesystem.File; + + public class OpenShapeCommand extends WorkerCommand + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function OpenShapeCommand(file:File) + { + super(file); + } + } +} diff --git a/editor/src/com/mapgen/commands/ProgressCommand.as b/editor/src/com/mapgen/commands/ProgressCommand.as new file mode 100644 index 0000000..39c557e --- /dev/null +++ b/editor/src/com/mapgen/commands/ProgressCommand.as @@ -0,0 +1,32 @@ +/* +* Copyright (c) 2015 Nailson Santos +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +* either express or implied. See the License for the specific language +* governing permissions and limitations under the License. +*/ + +package com.mapgen.commands +{ + import com.mignari.workers.WorkerCommand; + + public class ProgressCommand extends WorkerCommand + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function ProgressCommand(id:String, value:uint, total:uint, label:String) + { + super(id, value, total, label); + } + } +} diff --git a/editor/src/com/mapgen/commands/SaveMapCommand.as b/editor/src/com/mapgen/commands/SaveMapCommand.as new file mode 100644 index 0000000..6b33366 --- /dev/null +++ b/editor/src/com/mapgen/commands/SaveMapCommand.as @@ -0,0 +1,32 @@ +/* +* Copyright (c) 2015 Nailson Santos +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +* either express or implied. See the License for the specific language +* governing permissions and limitations under the License. +*/ + +package com.mapgen.commands +{ + import com.mignari.workers.WorkerCommand; + + public class SaveMapCommand extends WorkerCommand + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function SaveMapCommand() + { + super(); + } + } +} diff --git a/editor/src/com/mapgen/commands/SettingsCommand.as b/editor/src/com/mapgen/commands/SettingsCommand.as new file mode 100644 index 0000000..807925a --- /dev/null +++ b/editor/src/com/mapgen/commands/SettingsCommand.as @@ -0,0 +1,17 @@ +package com.mapgen.commands +{ + import com.mapgen.settings.MapShapeGenSettings; + import com.mignari.workers.WorkerCommand; + + public class SettingsCommand extends WorkerCommand + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function SettingsCommand(settings:MapShapeGenSettings) + { + super(settings); + } + } +} diff --git a/editor/src/com/mapgen/commands/UpdatePreviewCommand.as b/editor/src/com/mapgen/commands/UpdatePreviewCommand.as new file mode 100644 index 0000000..394af49 --- /dev/null +++ b/editor/src/com/mapgen/commands/UpdatePreviewCommand.as @@ -0,0 +1,32 @@ +/* +* Copyright (c) 2015 Nailson Santos +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +* either express or implied. See the License for the specific language +* governing permissions and limitations under the License. +*/ + +package com.mapgen.commands +{ + import com.mignari.workers.WorkerCommand; + + public class UpdatePreviewCommand extends WorkerCommand + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function UpdatePreviewCommand(islandSeed:String) + { + super(islandSeed); + } + } +} diff --git a/editor/src/com/mapgen/commands/UpdateShapeComamnd.as b/editor/src/com/mapgen/commands/UpdateShapeComamnd.as new file mode 100644 index 0000000..0aaa648 --- /dev/null +++ b/editor/src/com/mapgen/commands/UpdateShapeComamnd.as @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2015 Nailson S. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +package com.mapgen.commands +{ + import com.mignari.workers.WorkerCommand; + + public class UpdateShapeComamnd extends WorkerCommand + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function UpdateShapeComamnd(islandType:String, pointType:String, beach:Boolean, rivers:Boolean) + { + super(islandType, pointType, beach, rivers); + } + } +} diff --git a/editor/src/com/mapgen/components/AboutWIndow.mxml b/editor/src/com/mapgen/components/AboutWIndow.mxml new file mode 100644 index 0000000..523a5ad --- /dev/null +++ b/editor/src/com/mapgen/components/AboutWIndow.mxml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/editor/src/com/mapgen/components/Menu.as b/editor/src/com/mapgen/components/Menu.as new file mode 100644 index 0000000..40fb163 --- /dev/null +++ b/editor/src/com/mapgen/components/Menu.as @@ -0,0 +1,181 @@ +/* +* Copyright (c) 2016 Nailson S. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +package com.mapgen.components +{ + import com.mapgen.core.IMapShapeGen; + import com.mignari.menu.MenuItem; + import com.mignari.menu.NativeMenu; + import com.mignari.utils.DescriptorUtil; + + import mx.core.FlexGlobals; + import mx.events.FlexEvent; + + [ResourceBundle("mapgen_strings")] + + public class Menu extends NativeMenu + { + //-------------------------------------------------------------------------- + // PROPERTIES + //-------------------------------------------------------------------------- + + private var m_application:IMapShapeGen; + + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function Menu() + { + super(); + + m_application = IMapShapeGen(FlexGlobals.topLevelApplication); + m_application.addEventListener(FlexEvent.CREATION_COMPLETE, creationCompleteHandler); + } + + //-------------------------------------------------------------------------- + // METHODS + //-------------------------------------------------------------------------- + + //-------------------------------------- + // Private + //-------------------------------------- + + private function create():void + { + // root menu + var menu:MenuItem = new MenuItem(); + var macMenu:MenuItem; + + // separator + var separator:MenuItem = new MenuItem(); + + if (this.isMacOS) + { + macMenu = new MenuItem(); + macMenu.label = DescriptorUtil.getName(); + menu.addMenuItem(macMenu); + } + + // File + var fileMenu:MenuItem = new MenuItem(); + fileMenu.label = resourceManager.getString("mapgen_strings", "MENU_FILE"); + menu.addMenuItem(fileMenu); + + // File > New + var fileNewMenu:MenuItem = new MenuItem(); + fileNewMenu.label = resourceManager.getString("mapgen_strings", "MENU_FILE_NEW"); + fileNewMenu.data = FILE_NEW; + fileNewMenu.keyEquivalent = "N"; + fileNewMenu.controlKey = true; + fileMenu.addMenuItem(fileNewMenu); + + // File > Open + var fileOpenMenu:MenuItem = new MenuItem(); + fileOpenMenu.label = resourceManager.getString("mapgen_strings", "MENU_FILE_OPEN"); + fileOpenMenu.data = FILE_OPEN; + fileOpenMenu.keyEquivalent = "O"; + fileOpenMenu.controlKey = true; + fileMenu.addMenuItem(fileOpenMenu); + + // File > Generate + var fileGenerateMenu:MenuItem = new MenuItem(); + fileGenerateMenu.label = resourceManager.getString("mapgen_strings", "MENU_FILE_GENERATE"); + fileGenerateMenu.data = FILE_GENERATE; + fileGenerateMenu.keyEquivalent = "G"; + fileGenerateMenu.controlKey = true; + fileMenu.addMenuItem(fileGenerateMenu); + + // File > Save + var fileSaveMenu:MenuItem = new MenuItem(); + fileSaveMenu.label = resourceManager.getString("mapgen_strings", "MENU_FILE_SAVE"); + fileSaveMenu.data = FILE_SAVE; + fileSaveMenu.keyEquivalent = "S"; + fileSaveMenu.controlKey = true; + fileMenu.addMenuItem(fileSaveMenu); + + fileMenu.addMenuItem(separator); + + // File > Preferences + var filePreferencesMenu:MenuItem = new MenuItem(); + filePreferencesMenu.label = resourceManager.getString("mapgen_strings", "MENU_FILE_PREFERENCES"); + filePreferencesMenu.data = FILE_PREFERENCES; + filePreferencesMenu.keyEquivalent = "P"; + filePreferencesMenu.controlKey = true; + fileMenu.addMenuItem(filePreferencesMenu); + + // File > Exit + var fileExitMenu:MenuItem = new MenuItem(); + fileExitMenu.label = resourceManager.getString("mapgen_strings", "MENU_FILE_EXIT"); + fileExitMenu.data = FILE_EXIT; + fileExitMenu.keyEquivalent = "Q"; + fileExitMenu.controlKey = true; + + // Help + var helpMenu:MenuItem = new MenuItem(); + helpMenu.label = resourceManager.getString("mapgen_strings", "MENU_HELP"); + menu.addMenuItem(helpMenu); + + // Help > About + var aboutMenu:MenuItem = new MenuItem(); + aboutMenu.label = resourceManager.getString("mapgen_strings", "MENU_HELP_ABOUT") + " " + DescriptorUtil.getName(); + aboutMenu.data = HELP_ABOUT; + + if (this.isMacOS) + { + macMenu.addMenuItem(aboutMenu); + macMenu.addMenuItem(separator); + macMenu.addMenuItem(fileExitMenu); + } + else + { + fileMenu.addMenuItem(separator); + fileMenu.addMenuItem(fileExitMenu); + helpMenu.addMenuItem(aboutMenu); + } + + this.dataProvider = menu.serialize(); + } + + //-------------------------------------- + // Event Handlers + //-------------------------------------- + + protected function creationCompleteHandler(event:FlexEvent):void + { + m_application.removeEventListener(FlexEvent.CREATION_COMPLETE, creationCompleteHandler); + create(); + } + + //-------------------------------------------------------------------------- + // STATIC + //-------------------------------------------------------------------------- + + static public const FILE_NEW:String = "FILE_NEW"; + static public const FILE_OPEN:String = "FILE_OPEN"; + static public const FILE_GENERATE:String = "FILE_GENERATE"; + static public const FILE_SAVE:String = "FILE_SAVE"; + static public const FILE_PREFERENCES:String = "FILE_PREFERENCES"; + static public const FILE_EXIT:String = "FILE_EXIT"; + static public const HELP_ABOUT:String = "HELP_ABOUT"; + } +} diff --git a/editor/src/com/mapgen/components/NewMapWindow.mxml b/editor/src/com/mapgen/components/NewMapWindow.mxml new file mode 100644 index 0000000..e9d2758 --- /dev/null +++ b/editor/src/com/mapgen/components/NewMapWindow.mxml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/editor/src/com/mapgen/components/PreferencesWindow.mxml b/editor/src/com/mapgen/components/PreferencesWindow.mxml new file mode 100644 index 0000000..acd57b6 --- /dev/null +++ b/editor/src/com/mapgen/components/PreferencesWindow.mxml @@ -0,0 +1,342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/editor/src/com/mapgen/components/StatusBar.mxml b/editor/src/com/mapgen/components/StatusBar.mxml new file mode 100644 index 0000000..4d797d4 --- /dev/null +++ b/editor/src/com/mapgen/components/StatusBar.mxml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/editor/src/com/mapgen/core/IMapShapeGen.as b/editor/src/com/mapgen/core/IMapShapeGen.as new file mode 100644 index 0000000..6ca872e --- /dev/null +++ b/editor/src/com/mapgen/core/IMapShapeGen.as @@ -0,0 +1,34 @@ +/* +* Copyright (c) 2016 Nailson S. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +package com.mapgen.core +{ + import flash.events.IEventDispatcher; + + import mx.core.IWindow; + + public interface IMapShapeGen extends IEventDispatcher, IWindow + { + function get mapCreated():Boolean; + function get mapSaving():Boolean; + } +} diff --git a/editor/src/com/mapgen/settings/MapShapeGenSettings.as b/editor/src/com/mapgen/settings/MapShapeGenSettings.as new file mode 100644 index 0000000..e6dee79 --- /dev/null +++ b/editor/src/com/mapgen/settings/MapShapeGenSettings.as @@ -0,0 +1,65 @@ +/* +* Copyright (c) 2015 Nailson S. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +package com.mapgen.settings +{ + import com.mapgen.IslandType; + import com.mapgen.PointType; + import com.mignari.settings.Settings; + + import flash.filesystem.File; + + public class MapShapeGenSettings extends Settings + { + //-------------------------------------------------------------------------- + // PROPERTIES + //-------------------------------------------------------------------------- + + public var maximized:Boolean = true; + + public var islandType:String = IslandType.PERLIN; + public var pointType:String = PointType.RELAXED; + public var showBeach:Boolean = true; + public var showRivers:Boolean = true; + + public var waterItem:uint = 4608; + public var sandItem:uint = 231; + public var grassItem:uint = 106; + + public var ouputDirectory:File = null; + public var savePNG:Boolean = true; + public var version:uint = 56; + + public var mapName:String = "Untitled"; + public var mapWidth:uint = 512; + public var mapHeight:uint = 512; + + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function MapShapeGenSettings() + { + super(); + } + } +} diff --git a/editor/src/com/mapgen/utils/MapInfo.as b/editor/src/com/mapgen/utils/MapInfo.as new file mode 100644 index 0000000..6af1c7c --- /dev/null +++ b/editor/src/com/mapgen/utils/MapInfo.as @@ -0,0 +1,52 @@ +/* +* Copyright (c) 2015 Nailson S. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +package com.mapgen.utils +{ + public class MapInfo + { + //-------------------------------------------------------------------------- + // PROPERTIES + //-------------------------------------------------------------------------- + + public var name:String; + public var width:uint; + public var height:uint; + public var seed:String; + public var islandType:String; + public var pointType:String; + public var beach:Boolean; + public var rivers:Boolean; + public var created:Boolean; + public var changed:Boolean; + public var saving:Boolean; + + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function MapInfo() + { + //// + } + } +} diff --git a/editor/src/com/mapgen/utils/Version.as b/editor/src/com/mapgen/utils/Version.as new file mode 100644 index 0000000..4bee04b --- /dev/null +++ b/editor/src/com/mapgen/utils/Version.as @@ -0,0 +1,22 @@ +package com.mapgen.utils +{ + public final class Version + { + //-------------------------------------------------------------------------- + // PROPERTIES + //-------------------------------------------------------------------------- + + public var label:String; + public var otb:uint; + + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function Version(label:String, otb:uint) + { + this.label = label; + this.otb = otb; + } + } +} diff --git a/editor/src/icon/app/icon114.png b/editor/src/icon/app/icon114.png new file mode 100644 index 0000000..9aac979 Binary files /dev/null and b/editor/src/icon/app/icon114.png differ diff --git a/editor/src/icon/app/icon128.png b/editor/src/icon/app/icon128.png new file mode 100644 index 0000000..6d1f9df Binary files /dev/null and b/editor/src/icon/app/icon128.png differ diff --git a/editor/src/icon/app/icon144.png b/editor/src/icon/app/icon144.png new file mode 100644 index 0000000..d6ab516 Binary files /dev/null and b/editor/src/icon/app/icon144.png differ diff --git a/editor/src/icon/app/icon152.png b/editor/src/icon/app/icon152.png new file mode 100644 index 0000000..617419e Binary files /dev/null and b/editor/src/icon/app/icon152.png differ diff --git a/editor/src/icon/app/icon16.png b/editor/src/icon/app/icon16.png new file mode 100644 index 0000000..1903701 Binary files /dev/null and b/editor/src/icon/app/icon16.png differ diff --git a/editor/src/icon/app/icon192.png b/editor/src/icon/app/icon192.png new file mode 100644 index 0000000..0b595b5 Binary files /dev/null and b/editor/src/icon/app/icon192.png differ diff --git a/editor/src/icon/app/icon32.png b/editor/src/icon/app/icon32.png new file mode 100644 index 0000000..28b4d15 Binary files /dev/null and b/editor/src/icon/app/icon32.png differ diff --git a/editor/src/icon/app/icon36.png b/editor/src/icon/app/icon36.png new file mode 100644 index 0000000..99c4a40 Binary files /dev/null and b/editor/src/icon/app/icon36.png differ diff --git a/editor/src/icon/app/icon48.png b/editor/src/icon/app/icon48.png new file mode 100644 index 0000000..e7df6ab Binary files /dev/null and b/editor/src/icon/app/icon48.png differ diff --git a/editor/src/icon/app/icon72.png b/editor/src/icon/app/icon72.png new file mode 100644 index 0000000..031a773 Binary files /dev/null and b/editor/src/icon/app/icon72.png differ diff --git a/editor/src/icon/app/icon96.png b/editor/src/icon/app/icon96.png new file mode 100644 index 0000000..8628206 Binary files /dev/null and b/editor/src/icon/app/icon96.png differ diff --git a/editor/src/icon/system/icon128.png b/editor/src/icon/system/icon128.png new file mode 100644 index 0000000..32130a0 Binary files /dev/null and b/editor/src/icon/system/icon128.png differ diff --git a/editor/src/icon/system/icon16.png b/editor/src/icon/system/icon16.png new file mode 100644 index 0000000..1e8815d Binary files /dev/null and b/editor/src/icon/system/icon16.png differ diff --git a/editor/src/icon/system/icon32.png b/editor/src/icon/system/icon32.png new file mode 100644 index 0000000..52d827c Binary files /dev/null and b/editor/src/icon/system/icon32.png differ diff --git a/editor/src/icon/system/icon48.png b/editor/src/icon/system/icon48.png new file mode 100644 index 0000000..784f551 Binary files /dev/null and b/editor/src/icon/system/icon48.png differ diff --git a/editor/src/versions.xml b/editor/src/versions.xml new file mode 100644 index 0000000..f570d21 --- /dev/null +++ b/editor/src/versions.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/.actionScriptProperties b/library/.actionScriptProperties new file mode 100644 index 0000000..ccc06ac --- /dev/null +++ b/library/.actionScriptProperties @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/.flexLibProperties b/library/.flexLibProperties new file mode 100644 index 0000000..9dbf78d --- /dev/null +++ b/library/.flexLibProperties @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/library/.project b/library/.project new file mode 100644 index 0000000..65bd623 --- /dev/null +++ b/library/.project @@ -0,0 +1,18 @@ + + + mapgen + + + + + + com.adobe.flexbuilder.project.flexbuilder + + + + + + com.adobe.flexbuilder.project.aslibnature + com.adobe.flexbuilder.project.actionscriptnature + + diff --git a/library/libs/mignari_core.swc b/library/libs/mignari_core.swc new file mode 100644 index 0000000..c1ccbf5 Binary files /dev/null and b/library/libs/mignari_core.swc differ diff --git a/library/libs/otlib_core.swc b/library/libs/otlib_core.swc new file mode 100644 index 0000000..2f902de Binary files /dev/null and b/library/libs/otlib_core.swc differ diff --git a/library/src/MapgenClasses.as b/library/src/MapgenClasses.as new file mode 100644 index 0000000..a62ffe8 --- /dev/null +++ b/library/src/MapgenClasses.as @@ -0,0 +1,16 @@ +package +{ + internal final class MapgenClasses + { + import com.mapgen.Canvas; Canvas; + import com.mapgen.Generator; Generator; + import com.mapgen.IslandType; IslandType; + import com.mapgen.Map; Map; + import com.mapgen.NoisyEdges; NoisyEdges; + import com.mapgen.PointType; PointType; + import com.mapgen.graph.Center; Center; + import com.mapgen.graph.Corner; Corner; + import com.mapgen.graph.Edge; Edge; + import com.mapgen.utils.PointTypeUtil; PointTypeUtil; + } +} diff --git a/library/src/com/mapgen/BiomeColor.as b/library/src/com/mapgen/BiomeColor.as new file mode 100644 index 0000000..7bcff40 --- /dev/null +++ b/library/src/com/mapgen/BiomeColor.as @@ -0,0 +1,24 @@ +package com.mapgen +{ + import com.mignari.errors.AbstractClassError; + + public final class BiomeColor + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function BiomeColor() + { + throw new AbstractClassError(BiomeColor); + } + + //-------------------------------------------------------------------------- + // STATIC + //-------------------------------------------------------------------------- + + static public const WATER:uint = 0x336699; + static public const SAND:uint = 0xffCC99; + static public const GRASS:uint = 0x00CC00; + } +} diff --git a/library/src/com/mapgen/Canvas.as b/library/src/com/mapgen/Canvas.as new file mode 100644 index 0000000..9e5212b --- /dev/null +++ b/library/src/com/mapgen/Canvas.as @@ -0,0 +1,62 @@ +package com.mapgen +{ + import flash.display.BitmapData; + import flash.geom.Rectangle; + import flash.utils.ByteArray; + + public class Canvas + { + //-------------------------------------------------------------------------- + // PROPERTIES + //-------------------------------------------------------------------------- + + private var m_bitmap:BitmapData; + private var m_rect:Rectangle; + + //-------------------------------------- + // Getters / Setters + //-------------------------------------- + + public function get bitmap():BitmapData { return m_bitmap; } + + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function Canvas(width:int, height:int) + { + m_bitmap = new BitmapData(width, height, true, 0); + m_rect = new Rectangle(0, 0, width, height); + } + + //-------------------------------------------------------------------------- + // METHODS + //-------------------------------------------------------------------------- + + //-------------------------------------- + // Public + //-------------------------------------- + + public function draw(generator:Generator):void + { + m_bitmap.draw(generator); + } + + public function copyFromByteArray(input:ByteArray):void + { + input.position = 0; + m_bitmap.setPixels(m_rect, input); + } + + public function copyToByteArray(output:ByteArray):void + { + output.position = 0; + m_bitmap.copyPixelsToByteArray(m_rect, output); + } + + public function clear():void + { + m_bitmap.fillRect(m_rect, 0); + } + } +} diff --git a/library/src/com/mapgen/Generator.as b/library/src/com/mapgen/Generator.as new file mode 100644 index 0000000..de506c6 --- /dev/null +++ b/library/src/com/mapgen/Generator.as @@ -0,0 +1,519 @@ +// Draws the voronoi graph produced in Map.as +// Author: amitp@cs.stanford.edu +// Author: nailsonnego@gmail.com +// License: MIT + +package com.mapgen +{ + import com.mapgen.graph.Center; + import com.mapgen.graph.Edge; + import com.mignari.errors.FileNotFoundError; + import com.mignari.errors.NullArgumentError; + import com.mignari.errors.NullOrEmptyArgumentError; + import com.mignari.signals.ProgressSignal; + import com.mignari.signals.Signal0; + import com.mignari.utils.Color; + import com.mignari.utils.StringUtil; + import com.mignari.utils.isNullOrEmpty; + + import flash.display.BitmapData; + import flash.display.Graphics; + import flash.display.Shape; + import flash.display.Sprite; + import flash.filesystem.File; + import flash.filesystem.FileMode; + import flash.filesystem.FileStream; + import flash.geom.Point; + import flash.utils.ByteArray; + + import mx.graphics.codec.PNGEncoder; + + import otlib.map.Tile; + import otlib.otbm.OtbmWriter; + import otlib.otml.OTMLDocument; + + public class Generator extends Sprite + { + //-------------------------------------------------------------------------- + // PROPERTIES + //-------------------------------------------------------------------------- + + private var m_name:String; + private var m_width:uint; + private var m_height:uint; + private var m_islandSeed:String; + private var m_islandType:String; + private var m_pointType:String; + + private var m_mask:Shape; + + private var m_beachLayer:Shape; + private var m_riversLayer:Shape; + private var m_map:Map; + private var m_noisyEdges:NoisyEdges; + private var m_created:Boolean; + private var m_saving:Boolean; + + private var m_changedSignal:Signal0; + private var m_progressSignal:ProgressSignal; + + //-------------------------------------- + // Getters / Setters + //-------------------------------------- + + public function get created():Boolean { return m_created; } + public function get changed():Boolean { return false; } + public function get saving():Boolean { return m_saving; } + + public function get islandSeed():String { return m_islandSeed; } + public function get islandType():String { return m_islandType; } + public function get pointType():String { return m_pointType; } + public function get beach():Boolean { return m_beachLayer.visible; } + public function get rivers():Boolean { return m_riversLayer.visible; } + + public function get onChanged():Signal0 { return m_changedSignal; } + public function get onProgress():ProgressSignal { return m_progressSignal; } + + override public function get name():String { return m_name; } + override public function get width():Number { return m_width; } + override public function get height():Number { return m_height; } + + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function Generator() + { + m_width = 512; + m_height = 512; + m_mask = new Shape(); + m_map = new Map(m_width, m_height); + m_beachLayer = new Shape(); + addChild(m_beachLayer); + m_riversLayer = new Shape(); + addChild(m_riversLayer); + m_noisyEdges = new NoisyEdges(); + + m_changedSignal = new Signal0(); + m_progressSignal = new ProgressSignal(); + + this.mask = m_mask; + } + + //-------------------------------------------------------------------------- + // METHODS + //-------------------------------------------------------------------------- + + //-------------------------------------- + // Public + //-------------------------------------- + + public function load(file:File):void + { + if (!file) + { + throw new NullArgumentError("file"); + } + + if (!file.exists) + { + throw new FileNotFoundError(file); + } + + if (file.extension != "otshape") + { + throw new ArgumentError("Invalid shape file extension."); + } + + var doc:OTMLDocument = OTMLDocument.parse(file); + + create(doc.readAt("name", String), + doc.readAt("width", uint), + doc.readAt("height", uint), + doc.readAt("seed", String), + doc.readAt("island-type", String), + doc.readAt("point-type", String), + doc.readAt("beach", Boolean), + doc.readAt("rivers", Boolean)); + } + + public function create(name:String, + width:uint, + height:uint, + seed:String, + islandType:String, + pointType:String, + beach:Boolean, + rivers:Boolean):void + { + if (isNullOrEmpty(name)) + { + throw new NullOrEmptyArgumentError("name"); + } + + if (width == 0 || height == 0) + { + throw new ArgumentError("Invalid shape size."); + } + + if (isNullOrEmpty(seed)) + { + throw new NullOrEmptyArgumentError("seed"); + } + + if (isNullOrEmpty(islandType)) + { + throw new NullOrEmptyArgumentError("islandType"); + } + + if (isNullOrEmpty(pointType)) + { + throw new NullOrEmptyArgumentError("pointType"); + } + + m_name = name; + + if (m_width != width || + m_height != height || + m_islandSeed != seed || + m_islandType != islandType || + m_pointType != pointType) + { + if (m_width != width || m_height != height) + { + m_width = width; + m_height = height; + m_map.setSize(width, height); + } + + m_created = false; + m_islandSeed = seed; + m_islandType = islandType; + m_pointType = pointType; + + var progressId:String = StringUtil.randomKeyString(); + + clear(); + clearNoisyEdges(); + m_beachLayer.graphics.clear(); + m_riversLayer.graphics.clear(); + + drawMask(); + + m_progressSignal.dispatch(progressId, 0, 5, "Shaping map..."); + shapeMap(); + m_changedSignal.dispatch(); + + m_progressSignal.dispatch(progressId, 1, 5, "Placing points..."); + m_map.placePoints(); + m_changedSignal.dispatch(); + + m_progressSignal.dispatch(progressId, 2, 5, "Building graph..."); + m_map.buildGraph(); + m_map.assignBiomes(); + m_changedSignal.dispatch(); + + m_progressSignal.dispatch(progressId, 3, 5, "Features..."); + m_map.assignElevations(); + m_map.assignMoisture(); + m_map.assignBiomes(); + m_changedSignal.dispatch(); + + m_progressSignal.dispatch(progressId, 4, 5, "Rivers..."); + m_noisyEdges.buildNoisyEdges(m_map, m_map.mapRandom); + drawMap(); + + m_progressSignal.dispatch(progressId, 5, 5, ""); + } + + m_beachLayer.visible = beach; + m_riversLayer.visible = rivers; + m_created = true; + m_changedSignal.dispatch(); + } + + public function createBitmap():BitmapData + { + if (m_created) + { + var bitmap:BitmapData = new BitmapData(m_width, m_height, false, 0); + bitmap.draw(this); + return bitmap; + } + + return null; + } + + public function save(directory:File, water:uint, sand:uint, grass:uint, savePNG:Boolean = true, version:uint = 56):void + { + if (!directory) + { + throw new NullArgumentError("directory"); + } + + if (!directory.exists) + { + throw new FileNotFoundError(directory); + } + + if (!m_created || m_saving) + { + throw new Error("Internal erro"); + } + + m_saving = true; + + var bitmap:BitmapData = this.createBitmap(); + var bytes:ByteArray = new PNGEncoder().encode(bitmap); + bytes.position = 0; + + if (savePNG) + { + var pngFile:File = directory.resolvePath(m_name + ".png"); + var stream:FileStream = new FileStream(); + stream.open(pngFile, FileMode.WRITE); + stream.writeBytes(bytes, 0, bytes.bytesAvailable); + stream.close(); + } + + var color:Color = new Color(); + var width:uint = bitmap.width; + var height:uint = bitmap.height; + var tiles:Vector. = new Vector.(); + var value:uint = 0; + var total:uint = width * height; + var progressId:String = StringUtil.randomKeyString(); + var label:String = "Creating map..." + var start:uint = 1024 - (width / 2); + + for (var x:uint = 0; x < width; x++) + { + for (var y:uint = 0; y < height; y++) + { + var c:uint = bitmap.getPixel(x, y); + if (c != 0) + { + color.RGB = c; + var tile:Tile = new Tile(start + x, start + y, 7); + + if (color.R == 0xFF) + { + tile.ground = sand; + } + else if (color.G == 0xCC) + { + tile.ground = grass; + } + else + { + tile.ground = water; + } + + tiles[tiles.length] = tile; + } + + value++; + if ((value % 100) == 0) + { + m_progressSignal.dispatch(progressId, value, total, label); + } + } + } + + m_progressSignal.dispatch(progressId, total, total, label); + + var otbm:OtbmWriter = new OtbmWriter(); + otbm.onProgress.add(m_progressSignal.dispatch); + otbm.saveOtbm(directory.resolvePath(m_name + ".otbm"), tiles, version); + + var shapeFile:File = directory.resolvePath(m_name + ".otshape"); + var otshape:OTMLDocument = new OTMLDocument(); + otshape.tag = "Shape"; + otshape.writeAt("name", m_name); + otshape.writeAt("width", m_width); + otshape.writeAt("height", m_height); + otshape.writeAt("seed", m_islandSeed); + otshape.writeAt("island-type", m_islandType); + otshape.writeAt("point-type", m_pointType); + otshape.writeAt("beach", m_beachLayer.visible); + otshape.writeAt("rivers", m_riversLayer.visible); + otshape.save(shapeFile); + + otbm.onProgress.remove(m_progressSignal.dispatch); + m_saving = false; + } + + //-------------------------------------- + // Private + //-------------------------------------- + + // Random parameters governing the overall shape of the island + private function shapeMap():void + { + var seed:int = 0; + var variant:int = 0; + + if (m_islandSeed.length == 0) + { + m_islandSeed = generateSeed(); + } + + var match:Object = /\s*(\d+)(?:\-(\d+))\s*$/.exec(m_islandSeed); + if (match != null) + { + // It's of the format SHAPE-VARIANT + seed = parseInt(match[1]); + variant = parseInt(match[2] || "0"); + } + + if (seed == 0) + { + // Convert the string into a number. This is a cheesy way to + // do it but it doesn't matter. It just allows people to use + // words as seeds. + var length:uint = m_islandSeed.length; + for (var i:int = 0; i < length; i++) + { + seed = (seed << 4) | m_islandSeed.charCodeAt(i); + } + + seed %= 100000; + variant = 1 + Math.floor(9 * Math.random()); + } + + m_map.newIsland(m_islandType, m_pointType, seed, variant); + } + + private function drawMap():void + { + this.clear(); + this.renderPolygons(); + this.renderRivers(); + } + + private function drawMask():void + { + m_mask.graphics.clear(); + m_mask.graphics.beginFill(0x000000); + m_mask.graphics.drawRect(0, 0, m_width, m_height); + m_mask.graphics.endFill(); + } + + private function clearNoisyEdges():void + { + m_noisyEdges.clear(); + } + + private function clear():void + { + graphics.clear(); + graphics.beginFill(0, 0); + graphics.drawRect(0, 0, 2000, 2000); + graphics.endFill(); + graphics.beginFill(BiomeColor.WATER); + graphics.drawRect(0, 0, m_width, m_height); + graphics.endFill(); + } + + // Helper functions for rendering paths + private function drawPathForwards(graphics:Graphics, path:Vector.):void + { + for (var i:int = 0; i < path.length; i++) + { + graphics.lineTo(path[i].x, path[i].y); + } + } + + private function drawPathBackwards(graphics:Graphics, path:Vector.):void + { + for (var i:int = path.length-1; i >= 0; i--) + { + graphics.lineTo(path[i].x, path[i].y); + } + } + + public function renderRivers():void + { + var graphics:Graphics = m_riversLayer.graphics; + for each (var p:Center in m_map.centers) + { + for each (var r:Center in p.neighbors) + { + var edge:Edge = m_map.lookupEdgeFromCenter(p, r); + if (m_noisyEdges.path0[edge.index] != null && + m_noisyEdges.path1[edge.index] != null && + edge.river > 1 && + edge.river < 50) + { + graphics.lineStyle(edge.river, BiomeColor.WATER); + graphics.moveTo(m_noisyEdges.path0[edge.index][0].x, m_noisyEdges.path0[edge.index][0].y); + drawPathForwards(graphics, m_noisyEdges.path0[edge.index]); + drawPathBackwards(graphics, m_noisyEdges.path1[edge.index]); + + } + } + } + + graphics.endFill(); + } + + public function renderPolygons():void + { + graphics.beginFill(BiomeColor.WATER); + graphics.drawRect(0, 0, m_width, m_height); + graphics.endFill(); + + for each (var p:Center in m_map.centers) + { + for each (var r:Center in p.neighbors) + { + var edge:Edge = m_map.lookupEdgeFromCenter(p, r); + if (m_noisyEdges.path0[edge.index] != null && m_noisyEdges.path1[edge.index] != null) + { + var color:uint = p.biome; + if (color == BiomeColor.SAND) + { + m_beachLayer.graphics.beginFill(color); + drawPath0(p, edge, m_beachLayer.graphics); + drawPath1(p, edge, m_beachLayer.graphics); + m_beachLayer.graphics.endFill(); + color = BiomeColor.GRASS; + } + + this.graphics.beginFill(color); + drawPath0(p, edge, this.graphics); + drawPath1(p, edge, this.graphics); + this.graphics.endFill(); + } + } + } + } + + private function drawPath0(center:Center, edge:Edge, graphics:Graphics):void + { + var path:Vector. = m_noisyEdges.path0[edge.index]; + graphics.moveTo(center.point.x, center.point.y); + graphics.lineTo(path[0].x, path[0].y); + drawPathForwards(graphics, path); + graphics.lineTo(center.point.x, center.point.y); + } + + private function drawPath1(center:Center, edge:Edge, graphics:Graphics):void + { + var path:Vector. = m_noisyEdges.path1[edge.index]; + graphics.moveTo(center.point.x, center.point.y); + graphics.lineTo(path[0].x, path[0].y); + drawPathForwards(graphics, path); + graphics.lineTo(center.point.x, center.point.y); + } + + //-------------------------------------------------------------------------- + // STATIC + //-------------------------------------------------------------------------- + + static public function generateSeed():String + { + return int(Math.random() * 100000) + "-" + (1 + int(9 * Math.random())); + } + } +} diff --git a/library/src/com/mapgen/IslandShape.as b/library/src/com/mapgen/IslandShape.as new file mode 100644 index 0000000..d05978d --- /dev/null +++ b/library/src/com/mapgen/IslandShape.as @@ -0,0 +1,71 @@ +package com.mapgen +{ + import flash.display.BitmapData; + import flash.geom.Point; + + import de.polygonal.math.PM_PRNG; + + // This class has factory functions for generating islands of + // different shapes. The factory returns a function that takes a + // normalized point (x and y are -1 to +1) and returns true if the + // point should be on the island, and false if it should be water + // (lake or ocean). + internal final class IslandShape + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function IslandShape() + { + //// + } + + //-------------------------------------------------------------------------- + // STATIC + //-------------------------------------------------------------------------- + + // The radial island radius is based on overlapping sine waves + static public var ISLAND_FACTOR:Number = 1.07; // 1.0 means no small islands; 2.0 leads to a lot + static public function makeRadial(seed:int):Function + { + var islandRandom:PM_PRNG = new PM_PRNG(); + islandRandom.seed = seed; + var bumps:int = islandRandom.nextIntRange(1, 6); + var startAngle:Number = islandRandom.nextDoubleRange(0, 2*Math.PI); + var dipAngle:Number = islandRandom.nextDoubleRange(0, 2*Math.PI); + var dipWidth:Number = islandRandom.nextDoubleRange(0.2, 0.7); + + function inside(q:Point):Boolean + { + var angle:Number = Math.atan2(q.y, q.x); + var length:Number = 0.5 * (Math.max(Math.abs(q.x), Math.abs(q.y)) + q.length); + + var r1:Number = 0.5 + 0.40*Math.sin(startAngle + bumps*angle + Math.cos((bumps+3)*angle)); + var r2:Number = 0.7 - 0.20*Math.sin(startAngle + bumps*angle - Math.sin((bumps+2)*angle)); + if (Math.abs(angle - dipAngle) < dipWidth + || Math.abs(angle - dipAngle + 2*Math.PI) < dipWidth + || Math.abs(angle - dipAngle - 2*Math.PI) < dipWidth) { + r1 = r2 = 0.2; + } + + return (length < r1 || (length > r1*ISLAND_FACTOR && length < r2)); + } + + return inside; + } + + + // The Perlin-based island combines perlin noise with the radius + static public function makePerlin(seed:int):Function + { + var perlin:BitmapData = new BitmapData(256, 256); + perlin.perlinNoise(64, 64, 8, seed, false, true); + + return function (q:Point):Boolean { + var c:Number = (perlin.getPixel(int((q.x+1)*128), int((q.y+1)*128)) & 0xff) / 255.0; + return c > (0.3+0.3*q.length*q.length); + }; + } + } +} diff --git a/library/src/com/mapgen/IslandType.as b/library/src/com/mapgen/IslandType.as new file mode 100644 index 0000000..9543bce --- /dev/null +++ b/library/src/com/mapgen/IslandType.as @@ -0,0 +1,46 @@ +package com.mapgen +{ + import com.mignari.errors.AbstractClassError; + import com.mignari.utils.StringUtil; + + public final class IslandType + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function IslandType() + { + throw new AbstractClassError(IslandType); + } + + //-------------------------------------------------------------------------- + // STATIC + //-------------------------------------------------------------------------- + + static public const PERLIN:String = "perlin"; + static public const RADIAL:String = "radial"; + + static public function value(index:int):String + { + switch(index) + { + case 0: return PERLIN; + case 1: return RADIAL; + } + + return PERLIN; + } + + static public function index(value:String):int + { + switch(StringUtil.toKeyString(value)) + { + case PERLIN: return 0; + case RADIAL: return 1; + } + + return 0; + } + } +} diff --git a/library/src/com/mapgen/Map.as b/library/src/com/mapgen/Map.as new file mode 100644 index 0000000..64278f4 --- /dev/null +++ b/library/src/com/mapgen/Map.as @@ -0,0 +1,897 @@ +// Make a map out of a voronoi graph +// Author: amitp@cs.stanford.edu +// License: MIT + +package com.mapgen +{ + import com.mapgen.graph.Center; + import com.mapgen.graph.Corner; + import com.mapgen.graph.Edge; + import com.nodename.Delaunay.Voronoi; + import com.nodename.geom.LineSegment; + + import flash.geom.Point; + import flash.geom.Rectangle; + import flash.system.System; + import flash.utils.Dictionary; + + import de.polygonal.math.PM_PRNG; + + public class Map + { + //-------------------------------------------------------------------------- + // PROPERTIES + //-------------------------------------------------------------------------- + + private var m_width:uint; + private var m_height:uint; + + // Island shape is controlled by the islandRandom seed and the + // type of island, passed in when we set the island shape. The + // islandShape function uses both of them to determine whether any + // point should be water or land. + private var m_islandShape:Function; + + // Island details are controlled by this random generator. The + // initial map upon loading is always deterministic, but + // subsequent maps reset this random number generator with a + // random seed. + private var m_mapRandom:PM_PRNG = new PM_PRNG(); + private var m_needsMoreRandomness:Boolean; // see comment in PointSelector + + // Point selection is random for the original article, with Lloyd + // Relaxation, but there are other ways of choosing points. Grids + // in particular can be much simpler to start with, because you + // don't need Voronoi at all. HOWEVER for ease of implementation, + // I continue to use Voronoi here, to reuse the graph building + // code. If you're using a grid, generate the graph directly. + private var m_pointSelector:Function; + + // These store the graph data + public var points:Vector.; // Only useful during map construction + public var centers:Vector.
; + public var corners:Vector.; + public var edges:Vector.; + + //-------------------------------------- + // Getters / Setters + //-------------------------------------- + + public function get mapRandom():PM_PRNG { return m_mapRandom; } + + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function Map(width:uint, height:uint) + { + this.setSize(width, height); + + m_mapRandom = new PM_PRNG(); + } + + //-------------------------------------------------------------------------- + // METHODS + //-------------------------------------------------------------------------- + + //-------------------------------------- + // Public + //-------------------------------------- + + public function setSize(width:uint, height:uint):void + { + m_width = width; + m_height = height; + reset(); + } + + // Random parameters governing the overall shape of the island + public function newIsland(islandType:String, pointType:String, seed:int, variant:int):void + { + switch(islandType) + { + case IslandType.PERLIN: + m_islandShape = IslandShape.makePerlin(seed); + break; + + case IslandType.RADIAL: + m_islandShape = IslandShape.makeRadial(seed); + break; + + default: + throw new Error("Unknown island type " + islandType); + } + + switch(pointType) + { + case PointType.RELAXED: + m_pointSelector = PointSelector.generateRelaxed(m_width, seed); + break; + + case PointType.RANDOM: + m_pointSelector = PointSelector.generateRandom(m_width, seed); + break; + + case PointType.SQUARE: + m_pointSelector = PointSelector.generateSquare(m_width, seed); + break; + + case PointType.HEXAGON: + m_pointSelector = PointSelector.generateHexagon(m_width, seed); + break; + + default: + throw new Error("Unknown point type " + pointType); + } + + m_needsMoreRandomness = PointSelector.needsMoreRandomness(pointType); + m_mapRandom.seed = variant; + } + + public function placePoints():void + { + this.reset(); + this.points = m_pointSelector(2000); + } + + public function buildGraph():void + { + var voronoi:Voronoi = new Voronoi(points, null, new Rectangle(0, 0, m_width, m_height)); + internalBuildGraph(points, voronoi); + improveCorners(); + voronoi.dispose(); + voronoi = null; + points = null; + } + + public function assignElevations():void + { + assignCornerElevations(); + assignOceanCoastAndLand(); + redistributeElevations(landCorners(corners)); + + for each (var q:Corner in corners) + { + if (q.ocean || q.coast) + { + q.elevation = 0.0; + } + } + + assignPolygonElevations(); + } + + public function assignMoisture():void + { + calculateDownslopes(); + createRivers(); + assignCornerMoisture(); + redistributeMoisture(landCorners(corners)); + assignPolygonMoisture(); + } + + public function assignBiomes():void + { + for each (var p:Center in centers) + { + p.biome = getBiome(p); + } + } + + // Look up a Voronoi Edge object given two adjacent Voronoi + // polygons, or two adjacent Voronoi corners + public function lookupEdgeFromCenter(p:Center, r:Center):Edge + { + for each (var edge:Edge in p.borders) + { + if (edge.d0 == r || edge.d1 == r) + { + return edge; + } + } + + return null; + } + + //-------------------------------------- + // Private + //-------------------------------------- + + // Although Lloyd relaxation improves the uniformity of polygon + // sizes, it doesn't help with the edge lengths. Short edges can + // be bad for some games, and lead to weird artifacts on + // rivers. We can easily lengthen short edges by moving the + // corners, but **we lose the Voronoi property**. The corners are + // moved to the average of the polygon centers around them. Short + // edges become longer. Long edges tend to become shorter. The + // polygons tend to be more uniform after this step. + private function improveCorners():void + { + var newCorners:Vector. = new Vector.(corners.length); + + // First we compute the average of the centers next to each corner. + for each (var q:Corner in corners) + { + if (q.border) + { + newCorners[q.index] = q.point; + } + else + { + var point:Point = new Point(0.0, 0.0); + for each (var r:Center in q.touches) + { + point.x += r.point.x; + point.y += r.point.y; + } + + point.x /= q.touches.length; + point.y /= q.touches.length; + newCorners[q.index] = point; + } + } + + // Move the corners to the new locations. + for (var i:int = 0; i < corners.length; i++) + { + corners[i].point = newCorners[i]; + } + + // The edge midpoints were computed for the old corners and need + // to be recomputed. + for each (var edge:Edge in edges) + { + if (edge.v0 && edge.v1) + { + edge.midpoint = Point.interpolate(edge.v0.point, edge.v1.point, 0.5); + } + } + } + + // Create an array of corners that are on land only, for use by + // algorithms that work only on land. We return an array instead + // of a vector because the redistribution algorithms want to sort + // this array using Array.sortOn. + private function landCorners(corners:Vector.):Array + { + var locations:Array = []; + + for each (var corner:Corner in corners) + { + if (!corner.ocean && !corner.coast) + { + locations[locations.length] = corner; + } + } + + return locations; + } + + // Build graph data structure in 'edges', 'centers', 'corners', + // based on information in the Voronoi results: point.neighbors + // will be a list of neighboring points of the same type (corner + // or center); point.edges will be a list of edges that include + // that point. Each edge connects to four points: the Voronoi edge + // edge.{v0,v1} and its dual Delaunay triangle edge edge.{d0,d1}. + // For boundary polygons, the Delaunay edge will have one null + // point, and the Voronoi edge may be null. + private function internalBuildGraph(points:Vector., voronoi:Voronoi):void + { + var p:Center, q:Corner, point:Point, other:Point; + var libedges:Vector. = voronoi.edges(); + var centerLookup:Dictionary = new Dictionary(); + + // Build Center objects for each of the points, and a lookup map + // to find those Center objects again as we build the graph + for each (point in points) + { + p = new Center(); + p.index = centers.length; + p.point = point; + p.neighbors = new Vector.
(); + p.borders = new Vector.(); + p.corners = new Vector.(); + centers.push(p); + centerLookup[point] = p; + } + + // Workaround for Voronoi lib bug: we need to call region() + // before Edges or neighboringSites are available + for each (p in centers) + { + voronoi.region(p.point); + } + + // The Voronoi library generates multiple Point objects for + // corners, and we need to canonicalize to one Corner object. + // To make lookup fast, we keep an array of Points, bucketed by + // x value, and then we only have to look at other Points in + // nearby buckets. When we fail to find one, we'll create a new + // Corner object. + var _cornerMap:Array = []; + function makeCorner(point:Point):Corner + { + var q:Corner; + + if (point == null) + { + return null; + } + + for (var bucket:int = int(point.x)-1; bucket <= int(point.x)+1; bucket++) + { + for each (q in _cornerMap[bucket]) + { + var dx:Number = point.x - q.point.x; + var dy:Number = point.y - q.point.y; + if (dx*dx + dy*dy < 1e-6) + { + return q; + } + } + } + + bucket = int(point.x); + if (!_cornerMap[bucket]) + { + _cornerMap[bucket] = []; + } + + q = new Corner(); + q.index = corners.length; + corners.push(q); + q.point = point; + q.border = (point.x == 0 || point.x == m_width || point.y == 0 || point.y == m_height); + q.touches = new Vector.
(); + q.protrudes = new Vector.(); + q.adjacent = new Vector.(); + _cornerMap[bucket].push(q); + return q; + } + + // Helper functions for the following for loop; ideally these + // would be inlined + function addToCornerList(v:Vector., x:Corner):void + { + if (x != null && v.indexOf(x) < 0) + { + v[v.length] = x; + } + } + + function addToCenterList(v:Vector.
, x:Center):void + { + if (x != null && v.indexOf(x) < 0) + { + v[v.length] = x; + } + } + + for each (var libedge:com.nodename.Delaunay.Edge in libedges) + { + var dedge:LineSegment = libedge.delaunayLine(); + var vedge:LineSegment = libedge.voronoiEdge(); + + // Fill the graph data. Make an Edge object corresponding to + // the edge from the voronoi library. + var edge:Edge = new Edge(); + edge.index = edges.length; + edge.river = 0; + edges[edges.length] = edge; + edge.midpoint = vedge.p0 && vedge.p1 && Point.interpolate(vedge.p0, vedge.p1, 0.5); + + // Edges point to corners. Edges point to centers. + edge.v0 = makeCorner(vedge.p0); + edge.v1 = makeCorner(vedge.p1); + edge.d0 = centerLookup[dedge.p0]; + edge.d1 = centerLookup[dedge.p1]; + + // Centers point to edges. Corners point to edges. + if (edge.d0 != null) + { + edge.d0.borders.push(edge); + } + + if (edge.d1 != null) + { + edge.d1.borders.push(edge); + } + + if (edge.v0 != null) + { + edge.v0.protrudes.push(edge); + } + + if (edge.v1 != null) + { + edge.v1.protrudes.push(edge); + } + + // Centers point to centers. + if (edge.d0 != null && edge.d1 != null) + { + addToCenterList(edge.d0.neighbors, edge.d1); + addToCenterList(edge.d1.neighbors, edge.d0); + } + + // Corners point to corners + if (edge.v0 != null && edge.v1 != null) + { + addToCornerList(edge.v0.adjacent, edge.v1); + addToCornerList(edge.v1.adjacent, edge.v0); + } + + // Centers point to corners + if (edge.d0 != null) + { + addToCornerList(edge.d0.corners, edge.v0); + addToCornerList(edge.d0.corners, edge.v1); + } + + if (edge.d1 != null) + { + addToCornerList(edge.d1.corners, edge.v0); + addToCornerList(edge.d1.corners, edge.v1); + } + + // Corners point to centers + if (edge.v0 != null) + { + addToCenterList(edge.v0.touches, edge.d0); + addToCenterList(edge.v0.touches, edge.d1); + } + + if (edge.v1 != null) + { + addToCenterList(edge.v1.touches, edge.d0); + addToCenterList(edge.v1.touches, edge.d1); + } + } + } + + // Determine elevations and water at Voronoi corners. By + // construction, we have no local minima. This is important for + // the downslope vectors later, which are used in the river + // construction algorithm. Also by construction, inlets/bays + // push low elevation areas inland, which means many rivers end + // up flowing out through them. Also by construction, lakes + // often end up on river paths because they don't raise the + // elevation as much as other terrain does. + private function assignCornerElevations():void + { + var q:Corner, s:Corner; + var queue:Array = []; + + for each (q in corners) + { + q.water = !inside(q.point); + } + + for each (q in corners) + { + // The edges of the map are elevation 0 + if (q.border) + { + q.elevation = 0.0; + queue.push(q); + } + else + { + q.elevation = Infinity; + } + } + // Traverse the graph and assign elevations to each point. As we + // move away from the map border, increase the elevations. This + // guarantees that rivers always have a way down to the coast by + // going downhill (no local minima). + while (queue.length > 0) + { + q = queue.shift(); + + for each (s in q.adjacent) + { + // Every step up is epsilon over water or 1 over land. The + // number doesn't matter because we'll rescale the + // elevations later. + var newElevation:Number = 0.01 + q.elevation; + if (!q.water && !s.water) + { + newElevation += 1; + if (m_needsMoreRandomness) + { + // HACK: the map looks nice because of randomness of + // points, randomness of rivers, and randomness of + // edges. Without random point selection, I needed to + // inject some more randomness to make maps look + // nicer. I'm doing it here, with elevations, but I + // think there must be a better way. This hack is only + // used with square/hexagon grids. + newElevation += m_mapRandom.nextDouble(); + } + } + // If this point changed, we'll add it to the queue so + // that we can process its neighbors too. + if (newElevation < s.elevation) + { + s.elevation = newElevation; + queue.push(s); + } + } + } + } + + // Change the overall distribution of elevations so that lower + // elevations are more common than higher + // elevations. Specifically, we want elevation X to have frequency + // (1-X). To do this we will sort the corners, then set each + // corner to its desired elevation. + private function redistributeElevations(locations:Array):void + { + // SCALE_FACTOR increases the mountain area. At 1.0 the maximum + // elevation barely shows up on the map, so we set it to 1.1. + var SCALE_FACTOR:Number = 1.1; + var i:int, y:Number, x:Number; + + locations.sortOn('elevation', Array.NUMERIC); + for (i = 0; i < locations.length; i++) + { + // Let y(x) be the total area that we want at elevation <= x. + // We want the higher elevations to occur less than lower + // ones, and set the area to be y(x) = 1 - (1-x)^2. + y = i/(locations.length-1); + // Now we have to solve for x, given the known y. + // * y = 1 - (1-x)^2 + // * y = 1 - (1 - 2x + x^2) + // * y = 2x - x^2 + // * x^2 - 2x + y = 0 + // From this we can use the quadratic equation to get: + x = Math.sqrt(SCALE_FACTOR) - Math.sqrt(SCALE_FACTOR*(1-y)); + if (x > 1.0) + { + x = 1.0; // TODO: does this break downslopes? + } + locations[i].elevation = x; + } + } + + // Change the overall distribution of moisture to be evenly distributed. + private function redistributeMoisture(locations:Array):void + { + locations.sortOn('moisture', Array.NUMERIC); + for (var i:int = 0; i < locations.length; i++) + { + locations[i].moisture = i/(locations.length-1); + } + } + + // Determine polygon and corner types: ocean, coast, land. + private function assignOceanCoastAndLand():void + { + // Compute polygon attributes 'ocean' and 'water' based on the + // corner attributes. Count the water corners per + // polygon. Oceans are all polygons connected to the edge of the + // map. In the first pass, mark the edges of the map as ocean; + // in the second pass, mark any water-containing polygon + // connected an ocean as ocean. + var queue:Array = []; + var p:Center, q:Corner, r:Center, numWater:int; + + for each (p in centers) + { + numWater = 0; + for each (q in p.corners) + { + if (q.border) + { + p.border = true; + p.ocean = true; + q.water = true; + queue.push(p); + } + + if (q.water) + { + numWater += 1; + } + } + p.water = (p.ocean || numWater >= p.corners.length * LAKE_THRESHOLD); + } + + while (queue.length > 0) + { + p = queue.shift(); + for each (r in p.neighbors) + { + if (r.water && !r.ocean) + { + r.ocean = true; + queue.push(r); + } + } + } + + // Set the polygon attribute 'coast' based on its neighbors. If + // it has at least one ocean and at least one land neighbor, + // then this is a coastal polygon. + for each (p in centers) + { + var numOcean:int = 0; + var numLand:int = 0; + for each (r in p.neighbors) + { + numOcean += int(r.ocean); + numLand += int(!r.water); + } + + p.coast = (numOcean > 0) && (numLand > 0); + } + + + // Set the corner attributes based on the computed polygon + // attributes. If all polygons connected to this corner are + // ocean, then it's ocean; if all are land, then it's land; + // otherwise it's coast. + for each (q in corners) + { + numOcean = 0; + numLand = 0; + for each (p in q.touches) + { + numOcean += int(p.ocean); + numLand += int(!p.water); + } + + q.ocean = (numOcean == q.touches.length); + q.coast = (numOcean > 0) && (numLand > 0); + q.water = q.border || ((numLand != q.touches.length) && !q.coast); + } + } + + // Polygon elevations are the average of the elevations of their corners. + private function assignPolygonElevations():void + { + var p:Center, q:Corner, sumElevation:Number; + for each (p in centers) + { + sumElevation = 0.0; + for each (q in p.corners) + { + sumElevation += q.elevation; + } + + p.elevation = sumElevation / p.corners.length; + } + } + + // Calculate downslope pointers. At every point, we point to the + // point downstream from it, or to itself. This is used for + // generating rivers and watersheds. + private function calculateDownslopes():void + { + for each (var q:Corner in corners) + { + var r:Corner = q; + for each (var s:Corner in q.adjacent) + { + if (s.elevation <= r.elevation) + { + r = s; + } + } + + q.downslope = r; + } + } + + // Create rivers along edges. Pick a random corner point, then + // move downslope. Mark the edges and corners as rivers. + private function createRivers():void + { + var length:uint = m_width / 2; + for (var i:int = 0; i < length; i++) + { + var q:Corner = corners[m_mapRandom.nextIntRange(0, corners.length-1)]; + if (q.ocean || q.elevation < 0.3 || q.elevation > 0.9) + { + continue; + } + + // Bias rivers to go west: if (q.downslope.x > q.x) continue; + while (!q.coast) + { + if (q == q.downslope) + { + break; + } + + var edge:Edge = lookupEdgeFromCorner(q, q.downslope); + edge.river = edge.river + 1; + q.river = (q.river || 0) + 1; + q.downslope.river = (q.downslope.river || 0) + 1; // TODO: fix double count + q = q.downslope; + } + } + } + + // Calculate moisture. Freshwater sources spread moisture: rivers + // and lakes (not oceans). Saltwater sources have moisture but do + // not spread it (we set it at the end, after propagation). + private function assignCornerMoisture():void + { + var q:Corner; + var queue:Array = []; + + // Fresh water + for each (q in corners) + { + if ((q.water || q.river > 0) && !q.ocean) + { + q.moisture = q.river > 0? Math.min(3.0, (0.2 * q.river)) : 1.0; + queue.push(q); + } + else + { + q.moisture = 0.0; + } + } + + while (queue.length > 0) + { + q = queue.shift(); + + for each (var r:Corner in q.adjacent) + { + var newMoisture:Number = q.moisture * 0.9; + if (newMoisture > r.moisture) + { + r.moisture = newMoisture; + queue.push(r); + } + } + } + // Salt water + for each (q in corners) + { + if (q.ocean || q.coast) + { + q.moisture = 1.0; + } + } + } + + // Polygon moisture is the average of the moisture at corners + private function assignPolygonMoisture():void + { + for each (var p:Center in centers) + { + var sumMoisture:Number = 0.0; + for each (var q:Corner in p.corners) + { + if (q.moisture > 1.0) + { + q.moisture = 1.0; + } + + sumMoisture += q.moisture; + } + + p.moisture = sumMoisture / p.corners.length; + } + } + + private function lookupEdgeFromCorner(q:Corner, s:Corner):Edge + { + for each (var edge:Edge in q.protrudes) + { + if (edge.v0 == s || edge.v1 == s) + { + return edge; + } + } + + return null; + } + + private function reset():void + { + // Break cycles so the garbage collector will release data. + if (points) + { + points.splice(0, points.length); + } + + if (edges) + { + for each (var edge:Edge in edges) + { + edge.d0 = edge.d1 = null; + edge.v0 = edge.v1 = null; + } + + edges.splice(0, edges.length); + } + + if (centers) + { + for each (var p:Center in centers) + { + p.neighbors.splice(0, p.neighbors.length); + p.corners.splice(0, p.corners.length); + p.borders.splice(0, p.borders.length); + } + + centers.splice(0, centers.length); + } + + if (corners) + { + for each (var q:Corner in corners) + { + q.adjacent.splice(0, q.adjacent.length); + q.touches.splice(0, q.touches.length); + q.protrudes.splice(0, q.protrudes.length); + q.downslope = null; + q.watershed = null; + } + + corners.splice(0, corners.length); + } + + if (!points) + { + points = new Vector.(); + } + + if (!edges) + { + edges = new Vector.(); + } + + if (!centers) + { + centers = new Vector.
(); + } + + if (!corners) + { + corners = new Vector.(); + } + + System.gc(); + } + + // Determine whether a given point should be on the island or in the water. + private function inside(p:Point):Boolean + { + return m_islandShape(new Point(2 * (p.x / m_width - 0.5), 2 * (p.y / m_height - 0.5))); + } + + //-------------------------------------------------------------------------- + // STATIC + //-------------------------------------------------------------------------- + + // 0 to 1, fraction of water corners for water polygon + static public const LAKE_THRESHOLD:Number = 0.3; + + // Assign a biome type to each polygon. If it has + // ocean/coast/water, then that's the biome; otherwise it depends + // on low/high elevation and low/medium/high moisture. This is + // roughly based on the Whittaker diagram but adapted to fit the + // needs of the island map generator. + static private function getBiome(p:Center):uint + { + if (p.ocean || p.water) + { + return BiomeColor.WATER; + } + else if (p.coast) + { + return BiomeColor.SAND; + } + else + { + return BiomeColor.GRASS; + } + } + } +} diff --git a/library/src/com/mapgen/NoisyEdges.as b/library/src/com/mapgen/NoisyEdges.as new file mode 100644 index 0000000..87d1c87 --- /dev/null +++ b/library/src/com/mapgen/NoisyEdges.as @@ -0,0 +1,140 @@ +// Annotate each edge with a noisy path, to make maps look more interesting. +// Author: amitp@cs.stanford.edu +// License: MIT + +package com.mapgen +{ + import com.mapgen.graph.Center; + import com.mapgen.graph.Edge; + + import flash.geom.Point; + + import de.polygonal.math.PM_PRNG; + + public class NoisyEdges + { + //-------------------------------------------------------------------------- + // PROPERTIES + //-------------------------------------------------------------------------- + + public var path0:Array; // edge index -> Vector. + public var path1:Array; // edge index -> Vector. + + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function NoisyEdges() + { + this.path0 = []; + this.path1 = []; + } + + //-------------------------------------------------------------------------- + // METHODS + //-------------------------------------------------------------------------- + + //-------------------------------------- + // Public + //-------------------------------------- + + // Build noisy line paths for each of the Voronoi edges. There are + // two noisy line paths for each edge, each covering half the + // distance: path0 is from v0 to the midpoint and path1 is from v1 + // to the midpoint. When drawing the polygons, one or the other + // must be drawn in reverse order. + public function buildNoisyEdges(map:Map, random:PM_PRNG):void + { + for each (var p:Center in map.centers) + { + for each (var edge:Edge in p.borders) + { + if (edge.d0 && edge.d1 && edge.v0 && edge.v1 && !this.path0[edge.index]) + { + var f:Number = NOISY_LINE_TRADEOFF; + var t:Point = Point.interpolate(edge.v0.point, edge.d0.point, f); + var q:Point = Point.interpolate(edge.v0.point, edge.d1.point, f); + var r:Point = Point.interpolate(edge.v1.point, edge.d0.point, f); + var s:Point = Point.interpolate(edge.v1.point, edge.d1.point, f); + + var minLength:int = 10; + if (edge.d0.biome != edge.d1.biome) + { + minLength = 3; + } + + if (edge.d0.ocean && edge.d1.ocean) + { + minLength = 100; + } + + if (edge.d0.coast || edge.d1.coast) + { + minLength = 1; + } + + if (edge.river) + { + minLength = 1; + } + + this.path0[edge.index] = buildNoisyLineSegments(random, edge.v0.point, t, edge.midpoint, q, minLength); + this.path1[edge.index] = buildNoisyLineSegments(random, edge.v1.point, s, edge.midpoint, r, minLength); + } + } + } + } + + public function clear():void + { + this.path0 = []; + this.path1 = []; + } + + //-------------------------------------------------------------------------- + // STATIC + //-------------------------------------------------------------------------- + + static private var NOISY_LINE_TRADEOFF:Number = 0.5; // low: jagged vedge; high: jagged dedge + + // Helper function: build a single noisy line in a quadrilateral A-B-C-D, + // and store the output points in a Vector. + static private function buildNoisyLineSegments(random:PM_PRNG, A:Point, B:Point, C:Point, D:Point, minLength:Number):Vector. + { + function subdivide(A:Point, B:Point, C:Point, D:Point):void + { + if (A.subtract(C).length < minLength || B.subtract(D).length < minLength) + { + return; + } + + // Subdivide the quadrilateral + var p:Number = random.nextDoubleRange(0.2, 0.8); // vertical (along A-D and B-C) + var q:Number = random.nextDoubleRange(0.2, 0.8); // horizontal (along A-B and D-C) + + // Midpoints + var E:Point = Point.interpolate(A, D, p); + var F:Point = Point.interpolate(B, C, p); + var G:Point = Point.interpolate(A, B, q); + var I:Point = Point.interpolate(D, C, q); + + // Central point + var H:Point = Point.interpolate(E, F, q); + + // Divide the quad into subquads, but meet at H + var s:Number = 1.0 - random.nextDoubleRange(-0.4, +0.4); + var t:Number = 1.0 - random.nextDoubleRange(-0.4, +0.4); + + subdivide(A, Point.interpolate(G, B, s), H, Point.interpolate(E, D, t)); + points[points.length] = H; + subdivide(H, Point.interpolate(F, C, s), C, Point.interpolate(I, D, t)); + } + + var points:Vector. = new Vector.(); + points[points.length] = A; + subdivide(A, B, C, D); + points[points.length] = C; + return points; + } + } +} diff --git a/library/src/com/mapgen/PointSelector.as b/library/src/com/mapgen/PointSelector.as new file mode 100644 index 0000000..4e20560 --- /dev/null +++ b/library/src/com/mapgen/PointSelector.as @@ -0,0 +1,141 @@ +package com.mapgen +{ + import com.nodename.Delaunay.Voronoi; + + import flash.geom.Point; + import flash.geom.Rectangle; + + import de.polygonal.math.PM_PRNG; + + internal final class PointSelector + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function PointSelector() + { + //// + } + + //-------------------------------------------------------------------------- + // STATIC + //-------------------------------------------------------------------------- + + static public var NUM_LLOYD_RELAXATIONS:int = 2; + + // The square and hex grid point selection remove randomness from + // where the points are; we need to inject more randomness elsewhere + // to make the maps look better. I do this in the corner + // elevations. However I think more experimentation is needed. + static public function needsMoreRandomness(type:String):Boolean + { + return type == PointType.SQUARE || type == PointType.HEXAGON; + } + + // Generate points at random locations + static public function generateRandom(size:int, seed:int):Function + { + return function(numPoints:int):Vector. + { + var mapRandom:PM_PRNG = new PM_PRNG(); + mapRandom.seed = seed; + + var points:Vector. = new Vector.(numPoints, true); + for (var i:int = 0; i < numPoints; i++) + { + var point:Point = new Point(mapRandom.nextDoubleRange(10, size - 10), + mapRandom.nextDoubleRange(10, size - 10)); + points[i] = point; + } + + return points; + } + } + + // Improve the random set of points with Lloyd Relaxation + static public function generateRelaxed(size:int, seed:int):Function + { + return function(numPoints:int):Vector. + { + // We'd really like to generate "blue noise". Algorithms: + // 1. Poisson dart throwing: check each new point against all + // existing points, and reject it if it's too close. + // 2. Start with a hexagonal grid and randomly perturb points. + // 3. Lloyd Relaxation: move each point to the centroid of the + // generated Voronoi polygon, then generate Voronoi again. + // 4. Use force-based layout algorithms to push points away. + // 5. More at http://www.cs.virginia.edu/~gfx/pubs/antimony/ + // Option 3 is implemented here. If it's run for too many iterations, + // it will turn into a grid, but convergence is very slow, and we only + // run it a few times. + + var points:Vector. = generateRandom(size, seed)(numPoints); + for (var i:int = 0; i < NUM_LLOYD_RELAXATIONS; i++) + { + var voronoi:Voronoi = new Voronoi(points, null, new Rectangle(0, 0, size, size)); + for each (var p:Point in points) + { + var region:Vector. = voronoi.region(p); + p.x = 0.0; + p.y = 0.0; + + for each (var q:Point in region) + { + p.x += q.x; + p.y += q.y; + } + + p.x /= region.length; + p.y /= region.length; + region.splice(0, region.length); + } + + voronoi.dispose(); + } + + return points; + } + } + + // Generate points on a square grid + static public function generateSquare(size:int, seed:int):Function + { + return function(numPoints:int):Vector. + { + numPoints = Math.sqrt(numPoints); + + var points:Vector. = new Vector.(); + for (var x:int = 0; x < numPoints; x++) + { + for (var y:int = 0; y < numPoints; y++) + { + points[points.length] = new Point((0.5 + x) / numPoints * size, (0.5 + y) / numPoints * size); + } + } + + return points; + } + } + + // Generate points on a hexagon grid + static public function generateHexagon(size:int, seed:int):Function + { + return function(numPoints:int):Vector. + { + numPoints = Math.sqrt(numPoints); + + var points:Vector. = new Vector.(); + for (var x:int = 0; x < numPoints; x++) + { + for (var y:int = 0; y < numPoints; y++) + { + points[points.length] = new Point((0.5 + x) / numPoints * size, (0.25 + 0.5 * x % 2 + y) / numPoints * size); + } + } + + return points; + } + } + } +} diff --git a/library/src/com/mapgen/PointType.as b/library/src/com/mapgen/PointType.as new file mode 100644 index 0000000..1ac7a9c --- /dev/null +++ b/library/src/com/mapgen/PointType.as @@ -0,0 +1,52 @@ +package com.mapgen +{ + import com.mignari.errors.AbstractClassError; + import com.mignari.utils.StringUtil; + + public final class PointType + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function PointType() + { + throw new AbstractClassError(PointType); + } + + //-------------------------------------------------------------------------- + // STATIC + //-------------------------------------------------------------------------- + + static public const RELAXED:String = "relaxed"; + static public const RANDOM:String = "random"; + static public const SQUARE:String = "square"; + static public const HEXAGON:String = "hexagon"; + + static public function value(index:int):String + { + switch(index) + { + case 0: return RELAXED; + case 1: return RANDOM; + case 2: return SQUARE; + case 3: return HEXAGON; + } + + return RELAXED; + } + + static public function index(value:String):int + { + switch(StringUtil.toKeyString(value)) + { + case RELAXED: return 0; + case RANDOM: return 1; + case SQUARE: return 2; + case HEXAGON: return 3; + } + + return 0; + } + } +} diff --git a/library/src/com/mapgen/graph/Center.as b/library/src/com/mapgen/graph/Center.as new file mode 100644 index 0000000..9906bc4 --- /dev/null +++ b/library/src/com/mapgen/graph/Center.as @@ -0,0 +1,24 @@ +package com.mapgen.graph +{ + import flash.geom.Point; + + public class Center + { + //-------------------------------------------------------------------------- + // PROPERTIES + //-------------------------------------------------------------------------- + + public var index:int; + public var point:Point; // location + public var water:Boolean; // lake or ocean + public var ocean:Boolean; // ocean + public var coast:Boolean; // land polygon touching an ocean + public var border:Boolean; // at the edge of the map + public var biome:uint; // biome type (see article) + public var elevation:Number; // 0.0-1.0 + public var moisture:Number; // 0.0-1.0 + public var neighbors:Vector.
; + public var borders:Vector.; + public var corners:Vector.; + } +} diff --git a/library/src/com/mapgen/graph/Corner.as b/library/src/com/mapgen/graph/Corner.as new file mode 100644 index 0000000..2c388d7 --- /dev/null +++ b/library/src/com/mapgen/graph/Corner.as @@ -0,0 +1,30 @@ +package com.mapgen.graph +{ + import flash.geom.Point; + + public class Corner + { + //-------------------------------------------------------------------------- + // PROPERTIES + //-------------------------------------------------------------------------- + + public var index:int; + + public var point:Point; // location + public var ocean:Boolean; // ocean + public var water:Boolean; // lake or ocean + public var coast:Boolean; // touches ocean and land polygons + public var border:Boolean; // at the edge of the map + public var elevation:Number; // 0.0-1.0 + public var moisture:Number; // 0.0-1.0 + + public var touches:Vector.
; + public var protrudes:Vector.; + public var adjacent:Vector.; + + public var river:int; // 0 if no river, or volume of water in river + public var downslope:Corner; // pointer to adjacent corner most downhill + public var watershed:Corner; // pointer to coastal corner, or null + public var watershed_size:int; + } +} diff --git a/library/src/com/mapgen/graph/Edge.as b/library/src/com/mapgen/graph/Edge.as new file mode 100644 index 0000000..7123a17 --- /dev/null +++ b/library/src/com/mapgen/graph/Edge.as @@ -0,0 +1,27 @@ +package com.mapgen.graph +{ + import flash.geom.Point; + + public class Edge + { + //-------------------------------------------------------------------------- + // PROPERTIES + //-------------------------------------------------------------------------- + + public var index:int; + + // Delaunay edge + public var d0:Center; + public var d1:Center; + + // Voronoi edge + public var v0:Corner + public var v1:Corner; + + // halfway between v0,v1 + public var midpoint:Point; + + // volume of water, or 0 + public var river:int; + } +} diff --git a/library/src/com/mapgen/utils/PointTypeUtil.as b/library/src/com/mapgen/utils/PointTypeUtil.as new file mode 100644 index 0000000..2678c68 --- /dev/null +++ b/library/src/com/mapgen/utils/PointTypeUtil.as @@ -0,0 +1,60 @@ +/* +* Copyright (c) 2015 Nailson Santos +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +* either express or implied. See the License for the specific language +* governing permissions and limitations under the License. +*/ + +package com.mapgen.utils +{ + import com.mignari.errors.AbstractClassError; + + import flash.geom.Point; + + public final class PointTypeUtil + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function PointTypeUtil() + { + throw new AbstractClassError(PointTypeUtil); + } + + //-------------------------------------------------------------------------- + // STATIC + //-------------------------------------------------------------------------- + + /** + * Generate points on a hexagon grid. + */ + static public function generateHexagon(size:int, seed:int):Function + { + return function(numPoints:int):Vector. + { + numPoints = Math.sqrt(numPoints); + + var points:Vector. = new Vector.(); + for (var x:int = 0; x < numPoints; x++) + { + for (var y:int = 0; y < numPoints; y++) + { + points[points.length] = new Point((0.5 + x)/numPoints * size, (0.25 + 0.5*x%2 + y)/numPoints * size); + } + } + + return points; + } + } + } +} diff --git a/library/src/com/nodename/Delaunay/Edge.as b/library/src/com/nodename/Delaunay/Edge.as new file mode 100644 index 0000000..acf03f6 --- /dev/null +++ b/library/src/com/nodename/Delaunay/Edge.as @@ -0,0 +1,421 @@ +package com.nodename.Delaunay +{ + import com.nodename.geom.LineSegment; + + import flash.display.BitmapData; + import flash.display.CapsStyle; + import flash.display.Graphics; + import flash.display.LineScaleMode; + import flash.display.Sprite; + import flash.geom.Point; + import flash.geom.Rectangle; + import flash.utils.Dictionary; + + /** + * The line segment connecting the two Sites is part of the Delaunay triangulation; + * the line segment connecting the two Vertices is part of the Voronoi diagram + * @author ashaw + * + */ + public final class Edge + { + private static var _pool:Vector. = new Vector.(); + + /** + * This is the only way to create a new Edge + * @param site0 + * @param site1 + * @return + * + */ + internal static function createBisectingEdge(site0:Site, site1:Site):Edge + { + var dx:Number, dy:Number, absdx:Number, absdy:Number; + var a:Number, b:Number, c:Number; + + dx = site1.x - site0.x; + dy = site1.y - site0.y; + absdx = dx > 0 ? dx : -dx; + absdy = dy > 0 ? dy : -dy; + c = site0.x * dx + site0.y * dy + (dx * dx + dy * dy) * 0.5; + if (absdx > absdy) + { + a = 1.0; b = dy/dx; c /= dx; + } + else + { + b = 1.0; a = dx/dy; c /= dy; + } + + var edge:Edge = Edge.create(); + + edge.leftSite = site0; + edge.rightSite = site1; + site0.addEdge(edge); + site1.addEdge(edge); + + edge._leftVertex = null; + edge._rightVertex = null; + + edge.a = a; edge.b = b; edge.c = c; + //trace("createBisectingEdge: a ", edge.a, "b", edge.b, "c", edge.c); + + return edge; + } + + private static function create():Edge + { + var edge:Edge; + if (_pool.length > 0) + { + edge = _pool.pop(); + edge.init(); + } + else + { + edge = new Edge(PrivateConstructorEnforcer); + } + return edge; + } + + private static const LINESPRITE:Sprite = new Sprite(); + private static const GRAPHICS:Graphics = LINESPRITE.graphics; + + private var _delaunayLineBmp:BitmapData; + internal function get delaunayLineBmp():BitmapData + { + if (!_delaunayLineBmp) + { + _delaunayLineBmp = makeDelaunayLineBmp(); + } + return _delaunayLineBmp; + } + + // making this available to Voronoi; running out of memory in AIR so I cannot cache the bmp + internal function makeDelaunayLineBmp():BitmapData + { + var p0:Point = leftSite.coord; + var p1:Point = rightSite.coord; + + GRAPHICS.clear(); + // clear() resets line style back to undefined! + GRAPHICS.lineStyle(0, 0, 1.0, false, LineScaleMode.NONE, CapsStyle.NONE); + GRAPHICS.moveTo(p0.x, p0.y); + GRAPHICS.lineTo(p1.x, p1.y); + + var w:int = int(Math.ceil(Math.max(p0.x, p1.x))); + if (w < 1) + { + w = 1; + } + var h:int = int(Math.ceil(Math.max(p0.y, p1.y))); + if (h < 1) + { + h = 1; + } + var bmp:BitmapData = new BitmapData(w, h, true, 0); + bmp.draw(LINESPRITE); + return bmp; + } + + public function delaunayLine():LineSegment + { + // draw a line connecting the input Sites for which the edge is a bisector: + return new LineSegment(leftSite.coord, rightSite.coord); + } + + public function voronoiEdge():LineSegment + { + if (!visible) return new LineSegment(null, null); + return new LineSegment(_clippedVertices[LR.LEFT], + _clippedVertices[LR.RIGHT]); + } + + private static var _nedges:int = 0; + + internal static const DELETED:Edge = new Edge(PrivateConstructorEnforcer); + + // the equation of the edge: ax + by = c + internal var a:Number, b:Number, c:Number; + + // the two Voronoi vertices that the edge connects + // (if one of them is null, the edge extends to infinity) + private var _leftVertex:Vertex; + internal function get leftVertex():Vertex + { + return _leftVertex; + } + private var _rightVertex:Vertex; + internal function get rightVertex():Vertex + { + return _rightVertex; + } + internal function vertex(leftRight:LR):Vertex + { + return (leftRight == LR.LEFT) ? _leftVertex : _rightVertex; + } + internal function setVertex(leftRight:LR, v:Vertex):void + { + if (leftRight == LR.LEFT) + { + _leftVertex = v; + } + else + { + _rightVertex = v; + } + } + + internal function isPartOfConvexHull():Boolean + { + return (_leftVertex == null || _rightVertex == null); + } + + public function sitesDistance():Number + { + return Point.distance(leftSite.coord, rightSite.coord); + } + + public static function compareSitesDistances_MAX(edge0:Edge, edge1:Edge):Number + { + var length0:Number = edge0.sitesDistance(); + var length1:Number = edge1.sitesDistance(); + if (length0 < length1) + { + return 1; + } + if (length0 > length1) + { + return -1; + } + return 0; + } + + public static function compareSitesDistances(edge0:Edge, edge1:Edge):Number + { + return - compareSitesDistances_MAX(edge0, edge1); + } + + // Once clipVertices() is called, this Dictionary will hold two Points + // representing the clipped coordinates of the left and right ends... + private var _clippedVertices:Dictionary; + internal function get clippedEnds():Dictionary + { + return _clippedVertices; + } + // unless the entire Edge is outside the bounds. + // In that case visible will be false: + internal function get visible():Boolean + { + return _clippedVertices != null; + } + + // the two input Sites for which this Edge is a bisector: + private var _sites:Dictionary; + internal function set leftSite(s:Site):void + { + _sites[LR.LEFT] = s; + } + internal function get leftSite():Site + { + return _sites[LR.LEFT]; + } + internal function set rightSite(s:Site):void + { + _sites[LR.RIGHT] = s; + } + internal function get rightSite():Site + { + return _sites[LR.RIGHT] as Site; + } + internal function site(leftRight:LR):Site + { + return _sites[leftRight] as Site; + } + + private var _edgeIndex:int; + + public function dispose():void + { + if (_delaunayLineBmp) + { + _delaunayLineBmp.dispose(); + _delaunayLineBmp = null; + } + _leftVertex = null; + _rightVertex = null; + if (_clippedVertices) + { + _clippedVertices[LR.LEFT] = null; + _clippedVertices[LR.RIGHT] = null; + _clippedVertices = null; + } + _sites[LR.LEFT] = null; + _sites[LR.RIGHT] = null; + _sites = null; + + _pool.push(this); + } + + public function Edge(lock:Class) + { + if (lock != PrivateConstructorEnforcer) + { + throw new Error("Edge: constructor is private"); + } + + _edgeIndex = _nedges++; + init(); + } + + private function init():void + { + _sites = new Dictionary(true); + } + + public function toString():String + { + return "Edge " + _edgeIndex + "; sites " + _sites[LR.LEFT] + ", " + _sites[LR.RIGHT] + + "; endVertices " + (_leftVertex ? _leftVertex.vertexIndex : "null") + ", " + + (_rightVertex ? _rightVertex.vertexIndex : "null") + "::"; + } + + /** + * Set _clippedVertices to contain the two ends of the portion of the Voronoi edge that is visible + * within the bounds. If no part of the Edge falls within the bounds, leave _clippedVertices null. + * @param bounds + * + */ + internal function clipVertices(bounds:Rectangle):void + { + var xmin:Number = bounds.x; + var ymin:Number = bounds.y; + var xmax:Number = bounds.right; + var ymax:Number = bounds.bottom; + + var vertex0:Vertex, vertex1:Vertex; + var x0:Number, x1:Number, y0:Number, y1:Number; + + if (a == 1.0 && b >= 0.0) + { + vertex0 = _rightVertex; + vertex1 = _leftVertex; + } + else + { + vertex0 = _leftVertex; + vertex1 = _rightVertex; + } + + if (a == 1.0) + { + y0 = ymin; + if (vertex0 != null && vertex0.y > ymin) + { + y0 = vertex0.y; + } + if (y0 > ymax) + { + return; + } + x0 = c - b * y0; + + y1 = ymax; + if (vertex1 != null && vertex1.y < ymax) + { + y1 = vertex1.y; + } + if (y1 < ymin) + { + return; + } + x1 = c - b * y1; + + if ((x0 > xmax && x1 > xmax) || (x0 < xmin && x1 < xmin)) + { + return; + } + + if (x0 > xmax) + { + x0 = xmax; y0 = (c - x0)/b; + } + else if (x0 < xmin) + { + x0 = xmin; y0 = (c - x0)/b; + } + + if (x1 > xmax) + { + x1 = xmax; y1 = (c - x1)/b; + } + else if (x1 < xmin) + { + x1 = xmin; y1 = (c - x1)/b; + } + } + else + { + x0 = xmin; + if (vertex0 != null && vertex0.x > xmin) + { + x0 = vertex0.x; + } + if (x0 > xmax) + { + return; + } + y0 = c - a * x0; + + x1 = xmax; + if (vertex1 != null && vertex1.x < xmax) + { + x1 = vertex1.x; + } + if (x1 < xmin) + { + return; + } + y1 = c - a * x1; + + if ((y0 > ymax && y1 > ymax) || (y0 < ymin && y1 < ymin)) + { + return; + } + + if (y0 > ymax) + { + y0 = ymax; x0 = (c - y0)/a; + } + else if (y0 < ymin) + { + y0 = ymin; x0 = (c - y0)/a; + } + + if (y1 > ymax) + { + y1 = ymax; x1 = (c - y1)/a; + } + else if (y1 < ymin) + { + y1 = ymin; x1 = (c - y1)/a; + } + } + + _clippedVertices = new Dictionary(true); + if (vertex0 == _leftVertex) + { + _clippedVertices[LR.LEFT] = new Point(x0, y0); + _clippedVertices[LR.RIGHT] = new Point(x1, y1); + } + else + { + _clippedVertices[LR.RIGHT] = new Point(x0, y0); + _clippedVertices[LR.LEFT] = new Point(x1, y1); + } + } + + } +} + +class PrivateConstructorEnforcer {} diff --git a/library/src/com/nodename/Delaunay/EdgeList.as b/library/src/com/nodename/Delaunay/EdgeList.as new file mode 100644 index 0000000..9be5e07 --- /dev/null +++ b/library/src/com/nodename/Delaunay/EdgeList.as @@ -0,0 +1,176 @@ +package com.nodename.Delaunay +{ + import com.nodename.utils.IDisposable; + + import flash.geom.Point; + + internal final class EdgeList implements IDisposable + { + private var _deltax:Number; + private var _xmin:Number; + + private var _hashsize:int; + private var _hash:Vector.; + private var _leftEnd:Halfedge; + public function get leftEnd():Halfedge + { + return _leftEnd; + } + private var _rightEnd:Halfedge; + public function get rightEnd():Halfedge + { + return _rightEnd; + } + + public function dispose():void + { + var halfEdge:Halfedge = _leftEnd; + var prevHe:Halfedge; + while (halfEdge != _rightEnd) + { + prevHe = halfEdge; + halfEdge = halfEdge.edgeListRightNeighbor; + prevHe.dispose(); + } + _leftEnd = null; + _rightEnd.dispose(); + _rightEnd = null; + + var i:int; + for (i = 0; i < _hashsize; ++i) + { + _hash[i] = null; + } + _hash = null; + } + + public function EdgeList(xmin:Number, deltax:Number, sqrt_nsites:int) + { + _xmin = xmin; + _deltax = deltax; + _hashsize = 2 * sqrt_nsites; + + var i:int; + _hash = new Vector.(_hashsize, true); + + // two dummy Halfedges: + _leftEnd = Halfedge.createDummy(); + _rightEnd = Halfedge.createDummy(); + _leftEnd.edgeListLeftNeighbor = null; + _leftEnd.edgeListRightNeighbor = _rightEnd; + _rightEnd.edgeListLeftNeighbor = _leftEnd; + _rightEnd.edgeListRightNeighbor = null; + _hash[0] = _leftEnd; + _hash[_hashsize - 1] = _rightEnd; + } + + /** + * Insert newHalfedge to the right of lb + * @param lb + * @param newHalfedge + * + */ + public function insert(lb:Halfedge, newHalfedge:Halfedge):void + { + newHalfedge.edgeListLeftNeighbor = lb; + newHalfedge.edgeListRightNeighbor = lb.edgeListRightNeighbor; + lb.edgeListRightNeighbor.edgeListLeftNeighbor = newHalfedge; + lb.edgeListRightNeighbor = newHalfedge; + } + + /** + * This function only removes the Halfedge from the left-right list. + * We cannot dispose it yet because we are still using it. + * @param halfEdge + * + */ + public function remove(halfEdge:Halfedge):void + { + halfEdge.edgeListLeftNeighbor.edgeListRightNeighbor = halfEdge.edgeListRightNeighbor; + halfEdge.edgeListRightNeighbor.edgeListLeftNeighbor = halfEdge.edgeListLeftNeighbor; + halfEdge.edge = Edge.DELETED; + halfEdge.edgeListLeftNeighbor = halfEdge.edgeListRightNeighbor = null; + } + + /** + * Find the rightmost Halfedge that is still left of p + * @param p + * @return + * + */ + public function edgeListLeftNeighbor(p:Point):Halfedge + { + var i:int, bucket:int; + var halfEdge:Halfedge; + + /* Use hash table to get close to desired halfedge */ + bucket = (p.x - _xmin)/_deltax * _hashsize; + if (bucket < 0) + { + bucket = 0; + } + if (bucket >= _hashsize) + { + bucket = _hashsize - 1; + } + halfEdge = getHash(bucket); + if (halfEdge == null) + { + for (i = 1; true ; ++i) + { + if ((halfEdge = getHash(bucket - i)) != null) break; + if ((halfEdge = getHash(bucket + i)) != null) break; + } + } + /* Now search linear list of halfedges for the correct one */ + if (halfEdge == leftEnd || (halfEdge != rightEnd && halfEdge.isLeftOf(p))) + { + do + { + halfEdge = halfEdge.edgeListRightNeighbor; + } + while (halfEdge != rightEnd && halfEdge.isLeftOf(p)); + halfEdge = halfEdge.edgeListLeftNeighbor; + } + else + { + do + { + halfEdge = halfEdge.edgeListLeftNeighbor; + } + while (halfEdge != leftEnd && !halfEdge.isLeftOf(p)); + } + + /* Update hash table and reference counts */ + if (bucket > 0 && bucket <_hashsize - 1) + { + _hash[bucket] = halfEdge; + } + return halfEdge; + } + + /* Get entry from hash table, pruning any deleted nodes */ + private function getHash(b:int):Halfedge + { + var halfEdge:Halfedge; + + if (b < 0 || b >= _hashsize) + { + return null; + } + halfEdge = _hash[b]; + if (halfEdge != null && halfEdge.edge == Edge.DELETED) + { + /* Hash table points to deleted halfedge. Patch as necessary. */ + _hash[b] = null; + // still can't dispose halfEdge yet! + return null; + } + else + { + return halfEdge; + } + } + + } +} diff --git a/library/src/com/nodename/Delaunay/EdgeReorderer.as b/library/src/com/nodename/Delaunay/EdgeReorderer.as new file mode 100644 index 0000000..1f55738 --- /dev/null +++ b/library/src/com/nodename/Delaunay/EdgeReorderer.as @@ -0,0 +1,120 @@ +package com.nodename.Delaunay +{ + internal final class EdgeReorderer + { + private var _edges:Vector.; + private var _edgeOrientations:Vector.; + public function get edges():Vector. + { + return _edges; + } + public function get edgeOrientations():Vector. + { + return _edgeOrientations; + } + + public function EdgeReorderer(origEdges:Vector., criterion:Class) + { + if (criterion != Vertex && criterion != Site) + { + throw new ArgumentError("Edges: criterion must be Vertex or Site"); + } + _edges = new Vector.(); + _edgeOrientations = new Vector.(); + if (origEdges.length > 0) + { + _edges = reorderEdges(origEdges, criterion); + } + } + + public function dispose():void + { + _edges = null; + _edgeOrientations = null; + } + + private function reorderEdges(origEdges:Vector., criterion:Class):Vector. + { + var i:int; + var j:int; + var n:int = origEdges.length; + var edge:Edge; + // we're going to reorder the edges in order of traversal + var done:Vector. = new Vector.(n, true); + var nDone:int = 0; + for each (var b:Boolean in done) + { + b = false; + } + var newEdges:Vector. = new Vector.(); + + i = 0; + edge = origEdges[i]; + newEdges.push(edge); + _edgeOrientations.push(LR.LEFT); + var firstPoint:ICoord = (criterion == Vertex) ? edge.leftVertex : edge.leftSite; + var lastPoint:ICoord = (criterion == Vertex) ? edge.rightVertex : edge.rightSite; + + if (firstPoint == Vertex.VERTEX_AT_INFINITY || lastPoint == Vertex.VERTEX_AT_INFINITY) + { + return new Vector.(); + } + + done[i] = true; + ++nDone; + + while (nDone < n) + { + for (i = 1; i < n; ++i) + { + if (done[i]) + { + continue; + } + edge = origEdges[i]; + var leftPoint:ICoord = (criterion == Vertex) ? edge.leftVertex : edge.leftSite; + var rightPoint:ICoord = (criterion == Vertex) ? edge.rightVertex : edge.rightSite; + if (leftPoint == Vertex.VERTEX_AT_INFINITY || rightPoint == Vertex.VERTEX_AT_INFINITY) + { + return new Vector.(); + } + if (leftPoint == lastPoint) + { + lastPoint = rightPoint; + _edgeOrientations.push(LR.LEFT); + newEdges.push(edge); + done[i] = true; + } + else if (rightPoint == firstPoint) + { + firstPoint = leftPoint; + _edgeOrientations.unshift(LR.LEFT); + newEdges.unshift(edge); + done[i] = true; + } + else if (leftPoint == firstPoint) + { + firstPoint = rightPoint; + _edgeOrientations.unshift(LR.RIGHT); + newEdges.unshift(edge); + done[i] = true; + } + else if (rightPoint == lastPoint) + { + lastPoint = leftPoint; + _edgeOrientations.push(LR.RIGHT); + newEdges.push(edge); + done[i] = true; + } + if (done[i]) + { + ++nDone; + } + } + } + + return newEdges; + } + + } +} diff --git a/library/src/com/nodename/Delaunay/Halfedge.as b/library/src/com/nodename/Delaunay/Halfedge.as new file mode 100644 index 0000000..0a4a4a2 --- /dev/null +++ b/library/src/com/nodename/Delaunay/Halfedge.as @@ -0,0 +1,152 @@ +package com.nodename.Delaunay +{ + import flash.geom.Point; + + internal final class Halfedge + { + private static var _pool:Vector. = new Vector.(); + public static function create(edge:Edge, lr:LR):Halfedge + { + if (_pool.length > 0) + { + return _pool.pop().init(edge, lr); + } + else + { + return new Halfedge(PrivateConstructorEnforcer, edge, lr); + } + } + + public static function createDummy():Halfedge + { + return create(null, null); + } + + public var edgeListLeftNeighbor:Halfedge, edgeListRightNeighbor:Halfedge; + public var nextInPriorityQueue:Halfedge; + + public var edge:Edge; + public var leftRight:LR; + public var vertex:Vertex; + + // the vertex's y-coordinate in the transformed Voronoi space V* + public var ystar:Number; + + public function Halfedge(lock:Class, edge:Edge = null, lr:LR = null) + { + if (lock != PrivateConstructorEnforcer) + { + throw new Error("Halfedge constructor is private"); + } + + init(edge, lr); + } + + private function init(edge:Edge, lr:LR):Halfedge + { + this.edge = edge; + leftRight = lr; + nextInPriorityQueue = null; + vertex = null; + return this; + } + + public function toString():String + { + return "Halfedge (leftRight: " + leftRight + "; vertex: " + vertex + ")"; + } + + public function dispose():void + { + if (edgeListLeftNeighbor || edgeListRightNeighbor) + { + // still in EdgeList + return; + } + if (nextInPriorityQueue) + { + // still in PriorityQueue + return; + } + edge = null; + leftRight = null; + vertex = null; + _pool.push(this); + } + + public function reallyDispose():void + { + edgeListLeftNeighbor = null; + edgeListRightNeighbor = null; + nextInPriorityQueue = null; + edge = null; + leftRight = null; + vertex = null; + _pool.push(this); + } + + internal function isLeftOf(p:Point):Boolean + { + var topSite:Site; + var rightOfSite:Boolean, above:Boolean, fast:Boolean; + var dxp:Number, dyp:Number, dxs:Number, t1:Number, t2:Number, t3:Number, yl:Number; + + topSite = edge.rightSite; + rightOfSite = p.x > topSite.x; + if (rightOfSite && this.leftRight == LR.LEFT) + { + return true; + } + if (!rightOfSite && this.leftRight == LR.RIGHT) + { + return false; + } + + if (edge.a == 1.0) + { + dyp = p.y - topSite.y; + dxp = p.x - topSite.x; + fast = false; + if ((!rightOfSite && edge.b < 0.0) || (rightOfSite && edge.b >= 0.0) ) + { + above = dyp >= edge.b * dxp; + fast = above; + } + else + { + above = p.x + p.y * edge.b > edge.c; + if (edge.b < 0.0) + { + above = !above; + } + if (!above) + { + fast = true; + } + } + if (!fast) + { + dxs = topSite.x - edge.leftSite.x; + above = edge.b * (dxp * dxp - dyp * dyp) < + dxs * dyp * (1.0 + 2.0 * dxp/dxs + edge.b * edge.b); + if (edge.b < 0.0) + { + above = !above; + } + } + } + else /* edge.b == 1.0 */ + { + yl = edge.c - edge.a * p.x; + t1 = p.y - yl; + t2 = p.x - topSite.x; + t3 = yl - topSite.y; + above = t1 * t1 > t2 * t2 + t3 * t3; + } + return this.leftRight == LR.LEFT ? above : !above; + } + + } +} + +class PrivateConstructorEnforcer {} diff --git a/library/src/com/nodename/Delaunay/HalfedgePriorityQueue.as b/library/src/com/nodename/Delaunay/HalfedgePriorityQueue.as new file mode 100644 index 0000000..4e1543c --- /dev/null +++ b/library/src/com/nodename/Delaunay/HalfedgePriorityQueue.as @@ -0,0 +1,149 @@ +package com.nodename.Delaunay +{ + import flash.geom.Point; + + internal final class HalfedgePriorityQueue // also known as heap + { + private var _hash:Vector.; + private var _count:int; + private var _minBucket:int; + private var _hashsize:int; + + private var _ymin:Number; + private var _deltay:Number; + + public function HalfedgePriorityQueue(ymin:Number, deltay:Number, sqrt_nsites:int) + { + _ymin = ymin; + _deltay = deltay; + _hashsize = 4 * sqrt_nsites; + initialize(); + } + + public function dispose():void + { + // get rid of dummies + for (var i:int = 0; i < _hashsize; ++i) + { + _hash[i].dispose(); + _hash[i] = null; + } + _hash = null; + } + + private function initialize():void + { + var i:int; + + _count = 0; + _minBucket = 0; + _hash = new Vector.(_hashsize); + // dummy Halfedge at the top of each hash + for (i = 0; i < _hashsize; ++i) + { + _hash[i] = Halfedge.createDummy(); + _hash[i].nextInPriorityQueue = null; + } + } + + public function insert(halfEdge:Halfedge):void + { + var previous:Halfedge, next:Halfedge; + var insertionBucket:int = bucket(halfEdge); + if (insertionBucket < _minBucket) + { + _minBucket = insertionBucket; + } + previous = _hash[insertionBucket]; + while ((next = previous.nextInPriorityQueue) != null + && (halfEdge.ystar > next.ystar || (halfEdge.ystar == next.ystar && halfEdge.vertex.x > next.vertex.x))) + { + previous = next; + } + halfEdge.nextInPriorityQueue = previous.nextInPriorityQueue; + previous.nextInPriorityQueue = halfEdge; + ++_count; + } + + public function remove(halfEdge:Halfedge):void + { + var previous:Halfedge; + var removalBucket:int = bucket(halfEdge); + + if (halfEdge.vertex != null) + { + previous = _hash[removalBucket]; + while (previous.nextInPriorityQueue != halfEdge) + { + previous = previous.nextInPriorityQueue; + } + previous.nextInPriorityQueue = halfEdge.nextInPriorityQueue; + _count--; + halfEdge.vertex = null; + halfEdge.nextInPriorityQueue = null; + halfEdge.dispose(); + } + } + + private function bucket(halfEdge:Halfedge):int + { + var theBucket:int = (halfEdge.ystar - _ymin)/_deltay * _hashsize; + if (theBucket < 0) theBucket = 0; + if (theBucket >= _hashsize) theBucket = _hashsize - 1; + return theBucket; + } + + private function isEmpty(bucket:int):Boolean + { + return (_hash[bucket].nextInPriorityQueue == null); + } + + /** + * move _minBucket until it contains an actual Halfedge (not just the dummy at the top); + * + */ + private function adjustMinBucket():void + { + while (_minBucket < _hashsize - 1 && isEmpty(_minBucket)) + { + ++_minBucket; + } + } + + public function empty():Boolean + { + return _count == 0; + } + + /** + * @return coordinates of the Halfedge's vertex in V*, the transformed Voronoi diagram + * + */ + public function min():Point + { + adjustMinBucket(); + var answer:Halfedge = _hash[_minBucket].nextInPriorityQueue; + return new Point(answer.vertex.x, answer.ystar); + } + + /** + * remove and return the min Halfedge + * @return + * + */ + public function extractMin():Halfedge + { + var answer:Halfedge; + + // get the first real Halfedge in _minBucket + answer = _hash[_minBucket].nextInPriorityQueue; + + _hash[_minBucket].nextInPriorityQueue = answer.nextInPriorityQueue; + _count--; + answer.nextInPriorityQueue = null; + + return answer; + } + + } +} diff --git a/library/src/com/nodename/Delaunay/ICoord.as b/library/src/com/nodename/Delaunay/ICoord.as new file mode 100644 index 0000000..10777ab --- /dev/null +++ b/library/src/com/nodename/Delaunay/ICoord.as @@ -0,0 +1,9 @@ +package com.nodename.Delaunay +{ + import flash.geom.Point; + + internal interface ICoord + { + function get coord():Point; + } +} diff --git a/library/src/com/nodename/Delaunay/LR.as b/library/src/com/nodename/Delaunay/LR.as new file mode 100644 index 0000000..6d1927a --- /dev/null +++ b/library/src/com/nodename/Delaunay/LR.as @@ -0,0 +1,32 @@ +package com.nodename.Delaunay +{ + public final class LR + { + public static const LEFT:LR = new LR(PrivateConstructorEnforcer, "left"); + public static const RIGHT:LR = new LR(PrivateConstructorEnforcer, "right"); + + private var _name:String; + + public function LR(lock:Class, name:String) + { + if (lock != PrivateConstructorEnforcer) + { + throw new Error("Illegal constructor access"); + } + _name = name; + } + + public static function other(leftRight:LR):LR + { + return leftRight == LEFT ? RIGHT : LEFT; + } + + public function toString():String + { + return _name; + } + + } +} + +class PrivateConstructorEnforcer {} diff --git a/library/src/com/nodename/Delaunay/Site.as b/library/src/com/nodename/Delaunay/Site.as new file mode 100644 index 0000000..7209435 --- /dev/null +++ b/library/src/com/nodename/Delaunay/Site.as @@ -0,0 +1,466 @@ +package com.nodename.Delaunay +{ + import com.nodename.geom.Polygon; + import com.nodename.geom.Winding; + + import flash.geom.Point; + import flash.geom.Rectangle; + + public final class Site implements ICoord + { + private static var _pool:Vector. = new Vector.(); + public static function create(p:Point, index:int, weight:Number, color:uint):Site + { + if (_pool.length > 0) + { + return _pool.pop().init(p, index, weight, color); + } + else + { + return new Site(PrivateConstructorEnforcer, p, index, weight, color); + } + } + + internal static function sortSites(sites:Vector.):void + { + sites.sort(Site.compare); + } + + /** + * sort sites on y, then x, coord + * also change each site's _siteIndex to match its new position in the list + * so the _siteIndex can be used to identify the site for nearest-neighbor queries + * + * haha "also" - means more than one responsibility... + * + */ + private static function compare(s1:Site, s2:Site):Number + { + var returnValue:int = Voronoi.compareByYThenX(s1, s2); + + // swap _siteIndex values if necessary to match new ordering: + var tempIndex:int; + if (returnValue == -1) + { + if (s1._siteIndex > s2._siteIndex) + { + tempIndex = s1._siteIndex; + s1._siteIndex = s2._siteIndex; + s2._siteIndex = tempIndex; + } + } + else if (returnValue == 1) + { + if (s2._siteIndex > s1._siteIndex) + { + tempIndex = s2._siteIndex; + s2._siteIndex = s1._siteIndex; + s1._siteIndex = tempIndex; + } + + } + + return returnValue; + } + + + private static const EPSILON:Number = .005; + private static function closeEnough(p0:Point, p1:Point):Boolean + { + return Point.distance(p0, p1) < EPSILON; + } + + private var _coord:Point; + public function get coord():Point + { + return _coord; + } + + internal var color:uint; + internal var weight:Number; + + private var _siteIndex:uint; + + // the edges that define this Site's Voronoi region: + private var _edges:Vector.; + internal function get edges():Vector. + { + return _edges; + } + // which end of each edge hooks up with the previous edge in _edges: + private var _edgeOrientations:Vector.; + // ordered list of points that define the region clipped to bounds: + private var _region:Vector.; + + public function Site(lock:Class, p:Point, index:int, weight:Number, color:uint) + { + if (lock != PrivateConstructorEnforcer) + { + throw new Error("Site constructor is private"); + } + init(p, index, weight, color); + } + + private function init(p:Point, index:int, weight:Number, color:uint):Site + { + _coord = p; + _siteIndex = index; + this.weight = weight; + this.color = color; + _edges = new Vector.(); + _region = null; + return this; + } + + public function toString():String + { + return "Site " + _siteIndex + ": " + coord; + } + + private function move(p:Point):void + { + clear(); + _coord = p; + } + + public function dispose():void + { + _coord = null; + clear(); + _pool.push(this); + } + + private function clear():void + { + if (_edges) + { + _edges.length = 0; + _edges = null; + } + if (_edgeOrientations) + { + _edgeOrientations.length = 0; + _edgeOrientations = null; + } + if (_region) + { + _region.length = 0; + _region = null; + } + } + + internal function addEdge(edge:Edge):void + { + _edges.push(edge); + } + + internal function nearestEdge():Edge + { + _edges.sort(Edge.compareSitesDistances); + return _edges[0]; + } + + internal function neighborSites():Vector. + { + if (_edges == null || _edges.length == 0) + { + return new Vector.(); + } + if (_edgeOrientations == null) + { + reorderEdges(); + } + var list:Vector. = new Vector.(); + var edge:Edge; + for each (edge in _edges) + { + list.push(neighborSite(edge)); + } + return list; + } + + private function neighborSite(edge:Edge):Site + { + if (this == edge.leftSite) + { + return edge.rightSite; + } + if (this == edge.rightSite) + { + return edge.leftSite; + } + return null; + } + + internal function region(clippingBounds:Rectangle):Vector. + { + if (_edges == null || _edges.length == 0) + { + return new Vector.(); + } + if (_edgeOrientations == null) + { + reorderEdges(); + _region = clipToBounds(clippingBounds); + if ((new Polygon(_region)).winding() == Winding.CLOCKWISE) + { + _region = _region.reverse(); + } + } + return _region; + } + + private function reorderEdges():void + { + //trace("_edges:", _edges); + var reorderer:EdgeReorderer = new EdgeReorderer(_edges, Vertex); + _edges = reorderer.edges; + //trace("reordered:", _edges); + _edgeOrientations = reorderer.edgeOrientations; + reorderer.dispose(); + } + + private function clipToBounds(bounds:Rectangle):Vector. + { + var points:Vector. = new Vector.; + var n:int = _edges.length; + var i:int = 0; + var edge:Edge; + while (i < n && ((_edges[i] as Edge).visible == false)) + { + ++i; + } + + if (i == n) + { + // no edges visible + return new Vector.(); + } + edge = _edges[i]; + var orientation:LR = _edgeOrientations[i]; + points.push(edge.clippedEnds[orientation]); + points.push(edge.clippedEnds[LR.other(orientation)]); + + for (var j:int = i + 1; j < n; ++j) + { + edge = _edges[j]; + if (edge.visible == false) + { + continue; + } + connect(points, j, bounds); + } + // close up the polygon by adding another corner point of the bounds if needed: + connect(points, i, bounds, true); + + return points; + } + + private function connect(points:Vector., j:int, bounds:Rectangle, closingUp:Boolean = false):void + { + var rightPoint:Point = points[points.length - 1]; + var newEdge:Edge = _edges[j] as Edge; + var newOrientation:LR = _edgeOrientations[j]; + // the point that must be connected to rightPoint: + var newPoint:Point = newEdge.clippedEnds[newOrientation]; + if (!closeEnough(rightPoint, newPoint)) + { + // The points do not coincide, so they must have been clipped at the bounds; + // see if they are on the same border of the bounds: + if (rightPoint.x != newPoint.x + && rightPoint.y != newPoint.y) + { + // They are on different borders of the bounds; + // insert one or two corners of bounds as needed to hook them up: + // (NOTE this will not be correct if the region should take up more than + // half of the bounds rect, for then we will have gone the wrong way + // around the bounds and included the smaller part rather than the larger) + var rightCheck:int = BoundsCheck.check(rightPoint, bounds); + var newCheck:int = BoundsCheck.check(newPoint, bounds); + var px:Number, py:Number; + if (rightCheck & BoundsCheck.RIGHT) + { + px = bounds.right; + if (newCheck & BoundsCheck.BOTTOM) + { + py = bounds.bottom; + points.push(new Point(px, py)); + } + else if (newCheck & BoundsCheck.TOP) + { + py = bounds.top; + points.push(new Point(px, py)); + } + else if (newCheck & BoundsCheck.LEFT) + { + if (rightPoint.y - bounds.y + newPoint.y - bounds.y < bounds.height) + { + py = bounds.top; + } + else + { + py = bounds.bottom; + } + points.push(new Point(px, py)); + points.push(new Point(bounds.left, py)); + } + } + else if (rightCheck & BoundsCheck.LEFT) + { + px = bounds.left; + if (newCheck & BoundsCheck.BOTTOM) + { + py = bounds.bottom; + points.push(new Point(px, py)); + } + else if (newCheck & BoundsCheck.TOP) + { + py = bounds.top; + points.push(new Point(px, py)); + } + else if (newCheck & BoundsCheck.RIGHT) + { + if (rightPoint.y - bounds.y + newPoint.y - bounds.y < bounds.height) + { + py = bounds.top; + } + else + { + py = bounds.bottom; + } + points.push(new Point(px, py)); + points.push(new Point(bounds.right, py)); + } + } + else if (rightCheck & BoundsCheck.TOP) + { + py = bounds.top; + if (newCheck & BoundsCheck.RIGHT) + { + px = bounds.right; + points.push(new Point(px, py)); + } + else if (newCheck & BoundsCheck.LEFT) + { + px = bounds.left; + points.push(new Point(px, py)); + } + else if (newCheck & BoundsCheck.BOTTOM) + { + if (rightPoint.x - bounds.x + newPoint.x - bounds.x < bounds.width) + { + px = bounds.left; + } + else + { + px = bounds.right; + } + points.push(new Point(px, py)); + points.push(new Point(px, bounds.bottom)); + } + } + else if (rightCheck & BoundsCheck.BOTTOM) + { + py = bounds.bottom; + if (newCheck & BoundsCheck.RIGHT) + { + px = bounds.right; + points.push(new Point(px, py)); + } + else if (newCheck & BoundsCheck.LEFT) + { + px = bounds.left; + points.push(new Point(px, py)); + } + else if (newCheck & BoundsCheck.TOP) + { + if (rightPoint.x - bounds.x + newPoint.x - bounds.x < bounds.width) + { + px = bounds.left; + } + else + { + px = bounds.right; + } + points.push(new Point(px, py)); + points.push(new Point(px, bounds.top)); + } + } + } + if (closingUp) + { + // newEdge's ends have already been added + return; + } + points.push(newPoint); + } + var newRightPoint:Point = newEdge.clippedEnds[LR.other(newOrientation)]; + if (!closeEnough(points[0], newRightPoint)) + { + points.push(newRightPoint); + } + } + + internal function get x():Number + { + return _coord.x; + } + internal function get y():Number + { + return _coord.y; + } + + internal function dist(p:ICoord):Number + { + return Point.distance(p.coord, this._coord); + } + + } +} + +class PrivateConstructorEnforcer {} + +import flash.geom.Point; +import flash.geom.Rectangle; + +final class BoundsCheck +{ + public static const TOP:int = 1; + public static const BOTTOM:int = 2; + public static const LEFT:int = 4; + public static const RIGHT:int = 8; + + /** + * + * @param point + * @param bounds + * @return an int with the appropriate bits set if the Point lies on the corresponding bounds lines + * + */ + public static function check(point:Point, bounds:Rectangle):int + { + var value:int = 0; + if (point.x == bounds.left) + { + value |= LEFT; + } + if (point.x == bounds.right) + { + value |= RIGHT; + } + if (point.y == bounds.top) + { + value |= TOP; + } + if (point.y == bounds.bottom) + { + value |= BOTTOM; + } + return value; + } + + public function BoundsCheck() + { + throw new Error("BoundsCheck constructor unused"); + } +} diff --git a/library/src/com/nodename/Delaunay/SiteList.as b/library/src/com/nodename/Delaunay/SiteList.as new file mode 100644 index 0000000..5a46dd9 --- /dev/null +++ b/library/src/com/nodename/Delaunay/SiteList.as @@ -0,0 +1,174 @@ +package com.nodename.Delaunay +{ + import com.nodename.geom.Circle; + import com.nodename.utils.IDisposable; + + import flash.display.BitmapData; + import flash.geom.Point; + import flash.geom.Rectangle; + + internal final class SiteList implements IDisposable + { + private var m_sites:Vector.; + private var m_currentIndex:uint; + private var m_sorted:Boolean; + + public function SiteList() + { + m_sites = new Vector.(); + m_sorted = false; + } + + public function dispose():void + { + if (m_sites) + { + for each (var site:Site in m_sites) + { + site.dispose(); + } + + m_sites.length = 0; + m_sites = null; + } + } + + public function push(site:Site):uint + { + m_sorted = false; + return m_sites[m_sites.length] = site; + } + + public function get length():uint + { + return m_sites.length; + } + + public function next():Site + { + if (!m_sorted) + { + throw new Error("SiteList::next(): sites have not been sorted"); + } + + if (m_currentIndex < m_sites.length) + { + return m_sites[m_currentIndex++]; + } + + return null; + } + + internal function getSitesBounds():Rectangle + { + if (!m_sorted) + { + Site.sortSites(m_sites); + m_currentIndex = 0; + m_sorted = true; + } + + var xmin:Number, xmax:Number, ymin:Number, ymax:Number; + if (m_sites.length == 0) + { + return new Rectangle(0, 0, 0, 0); + } + + xmin = Number.MAX_VALUE; + xmax = Number.MIN_VALUE; + for each (var site:Site in m_sites) + { + if (site.x < xmin) + { + xmin = site.x; + } + + if (site.x > xmax) + { + xmax = site.x; + } + } + + // here's where we assume that the sites have been sorted on y: + ymin = m_sites[0].y; + ymax = m_sites[m_sites.length - 1].y; + return new Rectangle(xmin, ymin, xmax - xmin, ymax - ymin); + } + + public function siteColors(referenceImage:BitmapData = null):Vector. + { + var colors:Vector. = new Vector.(m_sites.length, true); + for (var i:uint = 0; i < length; i++) + { + var site:Site = m_sites[i]; + colors[i] = referenceImage ? referenceImage.getPixel(site.x, site.y) : site.color; + } + + return colors; + } + + public function siteCoords():Vector. + { + var coords:Vector. = new Vector.(m_sites.length, true); + for (var i:uint = 0; i < length; i++) + { + coords[i] = m_sites[i].coord; + } + + return coords; + } + + /** + * + * @return the largest circle centered at each site that fits in its region; + * if the region is infinite, return a circle of radius 0. + * + */ + public function circles():Vector. + { + var circles:Vector. = new Vector.(m_sites.length, true); + for (var i:uint = 0; i < length; i++) + { + var site:Site = m_sites[i]; + var radius:Number = 0; + var nearestEdge:Edge = site.nearestEdge(); + + //!nearestEdge.isPartOfConvexHull() && (radius = nearestEdge.sitesDistance() * 0.5); + circles[i] = new Circle(site.x, site.y, radius); + } + + return circles; + } + + public function regions(plotBounds:Rectangle):Vector.> + { + var regions:Vector.> = new Vector.>(m_sites, true); + for (var i:uint = 0; i < length; i++) + { + regions[i] = m_sites[i].region(plotBounds); + } + + return regions; + } + + /** + * + * @param proximityMap a BitmapData whose regions are filled with the site index values; see PlanePointsCanvas::fillRegions() + * @param x + * @param y + * @return coordinates of nearest Site to (x, y) + * + */ + public function nearestSitePoint(proximityMap:BitmapData, x:Number, y:Number):Point + { + var index:uint = proximityMap.getPixel(x, y); + if (index > m_sites.length - 1) + { + return null; + } + + return m_sites[index].coord; + } + + } +} diff --git a/library/src/com/nodename/Delaunay/Triangle.as b/library/src/com/nodename/Delaunay/Triangle.as new file mode 100644 index 0000000..55f66f3 --- /dev/null +++ b/library/src/com/nodename/Delaunay/Triangle.as @@ -0,0 +1,23 @@ +package com.nodename.Delaunay +{ + public final class Triangle + { + private var _sites:Vector.; + public function get sites():Vector. + { + return _sites; + } + + public function Triangle(a:Site, b:Site, c:Site) + { + _sites = Vector.([ a, b, c ]); + } + + public function dispose():void + { + _sites.length = 0; + _sites = null; + } + + } +} diff --git a/library/src/com/nodename/Delaunay/Vertex.as b/library/src/com/nodename/Delaunay/Vertex.as new file mode 100644 index 0000000..bdad838 --- /dev/null +++ b/library/src/com/nodename/Delaunay/Vertex.as @@ -0,0 +1,138 @@ +package com.nodename.Delaunay +{ + import flash.geom.Point; + + internal final class Vertex extends Object implements ICoord + { + internal static const VERTEX_AT_INFINITY:Vertex = new Vertex(PrivateConstructorEnforcer, NaN, NaN); + + private static var _pool:Vector. = new Vector.(); + private static function create(x:Number, y:Number):Vertex + { + if (isNaN(x) || isNaN(y)) + { + return VERTEX_AT_INFINITY; + } + if (_pool.length > 0) + { + return _pool.pop().init(x, y); + } + else + { + return new Vertex(PrivateConstructorEnforcer, x, y); + } + } + + + private static var _nvertices:int = 0; + + private var _coord:Point; + public function get coord():Point + { + return _coord; + } + private var _vertexIndex:int; + public function get vertexIndex():int + { + return _vertexIndex; + } + + public function Vertex(lock:Class, x:Number, y:Number) + { + if (lock != PrivateConstructorEnforcer) + { + throw new Error("Vertex constructor is private"); + } + + init(x, y); + } + + private function init(x:Number, y:Number):Vertex + { + _coord = new Point(x, y); + return this; + } + + public function dispose():void + { + _coord = null; + _pool.push(this); + } + + public function setIndex():void + { + _vertexIndex = _nvertices++; + } + + public function toString():String + { + return "Vertex (" + _vertexIndex + ")"; + } + + /** + * This is the only way to make a Vertex + * + * @param halfedge0 + * @param halfedge1 + * @return + * + */ + public static function intersect(halfedge0:Halfedge, halfedge1:Halfedge):Vertex + { + var edge0:Edge, edge1:Edge, edge:Edge; + var halfedge:Halfedge; + var determinant:Number, intersectionX:Number, intersectionY:Number; + var rightOfSite:Boolean; + + edge0 = halfedge0.edge; + edge1 = halfedge1.edge; + if (edge0 == null || edge1 == null) + { + return null; + } + if (edge0.rightSite == edge1.rightSite) + { + return null; + } + + determinant = edge0.a * edge1.b - edge0.b * edge1.a; + if (-1.0e-10 < determinant && determinant < 1.0e-10) + { + // the edges are parallel + return null; + } + + intersectionX = (edge0.c * edge1.b - edge1.c * edge0.b)/determinant; + intersectionY = (edge1.c * edge0.a - edge0.c * edge1.a)/determinant; + + if (Voronoi.compareByYThenX(edge0.rightSite, edge1.rightSite) < 0) + { + halfedge = halfedge0; edge = edge0; + } + else + { + halfedge = halfedge1; edge = edge1; + } + rightOfSite = intersectionX >= edge.rightSite.x; + if ((rightOfSite && halfedge.leftRight == LR.LEFT) + || (!rightOfSite && halfedge.leftRight == LR.RIGHT)) + { + return null; + } + + return Vertex.create(intersectionX, intersectionY); + } + + public function get x():Number + { + return _coord.x; + } + public function get y():Number + { + return _coord.y; + } + + } +} + +class PrivateConstructorEnforcer {} diff --git a/library/src/com/nodename/Delaunay/Voronoi.as b/library/src/com/nodename/Delaunay/Voronoi.as new file mode 100644 index 0000000..b77052e --- /dev/null +++ b/library/src/com/nodename/Delaunay/Voronoi.as @@ -0,0 +1,428 @@ +/* +* The author of this software is Steven Fortune. Copyright (c) 1994 by AT&T +* Bell Laboratories. +* Permission to use, copy, modify, and distribute this software for any +* purpose without fee is hereby granted, provided that this entire notice +* is included in all copies of any software which is or includes a copy +* or modification of this software and in all copies of the supporting +* documentation for such software. +* THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED +* WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE ANY +* REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY +* OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. +*/ + +package com.nodename.Delaunay +{ + import com.nodename.geom.Circle; + import com.nodename.geom.LineSegment; + + import flash.display.BitmapData; + import flash.geom.Point; + import flash.geom.Rectangle; + import flash.utils.Dictionary; + + public final class Voronoi + { + private var _sites:SiteList; + private var _sitesIndexedByLocation:Dictionary; + private var _triangles:Vector.; + private var _edges:Vector.; + + // TODO generalize this so it doesn't have to be a rectangle; + // then we can make the fractal voronois-within-voronois + private var _plotBounds:Rectangle; + public function get plotBounds():Rectangle + { + return _plotBounds; + } + + public function dispose():void + { + var i:int, n:int; + if (_sites) + { + _sites.dispose(); + _sites = null; + } + + if (_triangles) + { + n = _triangles.length; + for (i = 0; i < n; ++i) + { + _triangles[i].dispose(); + } + _triangles.length = 0; + _triangles = null; + } + + if (_edges) + { + n = _edges.length; + for (i = 0; i < n; ++i) + { + _edges[i].dispose(); + } + + _edges.length = 0; + _edges = null; + } + + _plotBounds = null; + _sitesIndexedByLocation = null; + } + + public function Voronoi(points:Vector., colors:Vector., plotBounds:Rectangle) + { + _sites = new SiteList(); + _sitesIndexedByLocation = new Dictionary(true); + addSites(points, colors); + _plotBounds = plotBounds; + _triangles = new Vector.(); + _edges = new Vector.(); + fortunesAlgorithm(); + } + + private function addSites(points:Vector., colors:Vector.):void + { + var length:uint = points.length; + for (var i:uint = 0; i < length; ++i) + { + addSite(points[i], colors ? colors[i] : 0, i); + } + } + + private function addSite(p:Point, color:uint, index:int):void + { + var weight:Number = Math.random() * 100; + var site:Site = Site.create(p, index, weight, color); + _sites.push(site); + _sitesIndexedByLocation[p] = site; + } + + public function edges():Vector. + { + return _edges; + } + + public function region(p:Point):Vector. + { + var site:Site = _sitesIndexedByLocation[p]; + if (!site) + { + return new Vector.(); + } + return site.region(_plotBounds); + } + + // TODO: bug: if you call this before you call region(), something goes wrong :( + public function neighborSitesForSite(coord:Point):Vector. + { + var points:Vector. = new Vector.(); + var site:Site = _sitesIndexedByLocation[coord]; + if (!site) + { + return points; + } + var sites:Vector. = site.neighborSites(); + var neighbor:Site; + for each (neighbor in sites) + { + points.push(neighbor.coord); + } + return points; + } + + public function circles():Vector. + { + return _sites.circles(); + } + + public function voronoiBoundaryForSite(coord:Point):Vector. + { + return visibleLineSegments(selectEdgesForSitePoint(coord, _edges)); + } + + public function delaunayLinesForSite(coord:Point):Vector. + { + return delaunayLinesForEdges(selectEdgesForSitePoint(coord, _edges)); + } + + public function voronoiDiagram():Vector. + { + return visibleLineSegments(_edges); + } + + public function delaunayTriangulation(keepOutMask:BitmapData = null):Vector. + { + return delaunayLinesForEdges(selectNonIntersectingEdges(keepOutMask, _edges)); + } + + public function hull():Vector. + { + return delaunayLinesForEdges(hullEdges()); + } + + private function hullEdges():Vector. + { + return _edges.filter(myTest); + + function myTest(edge:Edge, index:int, vector:Vector.):Boolean + { + return (edge.isPartOfConvexHull()); + } + } + + public function hullPointsInOrder():Vector. + { + var hullEdges:Vector. = hullEdges(); + + var points:Vector. = new Vector.(); + if (hullEdges.length == 0) + { + return points; + } + + var reorderer:EdgeReorderer = new EdgeReorderer(hullEdges, Site); + hullEdges = reorderer.edges; + var orientations:Vector. = reorderer.edgeOrientations; + reorderer.dispose(); + + var orientation:LR; + + var n:int = hullEdges.length; + for (var i:int = 0; i < n; ++i) + { + var edge:Edge = hullEdges[i]; + orientation = orientations[i]; + points.push(edge.site(orientation).coord); + } + return points; + } + + public function spanningTree(type:String = "minimum", keepOutMask:BitmapData = null):Vector. + { + var edges:Vector. = selectNonIntersectingEdges(keepOutMask, _edges); + var segments:Vector. = delaunayLinesForEdges(edges); + return kruskal(segments, type); + } + + public function regions():Vector.> + { + return _sites.regions(_plotBounds); + } + + public function siteColors(referenceImage:BitmapData = null):Vector. + { + return _sites.siteColors(referenceImage); + } + + /** + * + * @param proximityMap a BitmapData whose regions are filled with the site index values; see PlanePointsCanvas::fillRegions() + * @param x + * @param y + * @return coordinates of nearest Site to (x, y) + * + */ + public function nearestSitePoint(proximityMap:BitmapData, x:Number, y:Number):Point + { + return _sites.nearestSitePoint(proximityMap, x, y); + } + + public function siteCoords():Vector. + { + return _sites.siteCoords(); + } + + private function fortunesAlgorithm():void + { + var newSite:Site, bottomSite:Site, topSite:Site, tempSite:Site; + var v:Vertex, vertex:Vertex; + var newintstar:Point; + var leftRight:LR; + var lbnd:Halfedge, rbnd:Halfedge, llbnd:Halfedge, rrbnd:Halfedge, bisector:Halfedge; + var edge:Edge; + + var dataBounds:Rectangle = _sites.getSitesBounds(); + + var sqrt_nsites:int = int(Math.sqrt(_sites.length + 4)); + var heap:HalfedgePriorityQueue = new HalfedgePriorityQueue(dataBounds.y, dataBounds.height, sqrt_nsites); + var edgeList:EdgeList = new EdgeList(dataBounds.x, dataBounds.width, sqrt_nsites); + var halfEdges:Vector. = new Vector.(); + var vertices:Vector. = new Vector.(); + + var bottomMostSite:Site = _sites.next(); + newSite = _sites.next(); + + for (;;) + { + if (heap.empty() == false) + { + newintstar = heap.min(); + } + + if (newSite != null + && (heap.empty() || compareByYThenX(newSite, newintstar) < 0)) + { + /* new site is smallest */ + //trace("smallest: new site " + newSite); + + // Step 8: + lbnd = edgeList.edgeListLeftNeighbor(newSite.coord); // the Halfedge just to the left of newSite + //trace("lbnd: " + lbnd); + rbnd = lbnd.edgeListRightNeighbor; // the Halfedge just to the right + //trace("rbnd: " + rbnd); + bottomSite = rightRegion(lbnd); // this is the same as leftRegion(rbnd) + // this Site determines the region containing the new site + //trace("new Site is in region of existing site: " + bottomSite); + + // Step 9: + edge = Edge.createBisectingEdge(bottomSite, newSite); + //trace("new edge: " + edge); + _edges.push(edge); + + bisector = Halfedge.create(edge, LR.LEFT); + halfEdges.push(bisector); + // inserting two Halfedges into edgeList constitutes Step 10: + // insert bisector to the right of lbnd: + edgeList.insert(lbnd, bisector); + + // first half of Step 11: + if ((vertex = Vertex.intersect(lbnd, bisector)) != null) + { + vertices.push(vertex); + heap.remove(lbnd); + lbnd.vertex = vertex; + lbnd.ystar = vertex.y + newSite.dist(vertex); + heap.insert(lbnd); + } + + lbnd = bisector; + bisector = Halfedge.create(edge, LR.RIGHT); + halfEdges.push(bisector); + // second Halfedge for Step 10: + // insert bisector to the right of lbnd: + edgeList.insert(lbnd, bisector); + + // second half of Step 11: + if ((vertex = Vertex.intersect(bisector, rbnd)) != null) + { + vertices.push(vertex); + bisector.vertex = vertex; + bisector.ystar = vertex.y + newSite.dist(vertex); + heap.insert(bisector); + } + + newSite = _sites.next(); + } + else if (heap.empty() == false) + { + /* intersection is smallest */ + lbnd = heap.extractMin(); + llbnd = lbnd.edgeListLeftNeighbor; + rbnd = lbnd.edgeListRightNeighbor; + rrbnd = rbnd.edgeListRightNeighbor; + bottomSite = leftRegion(lbnd); + topSite = rightRegion(rbnd); + // these three sites define a Delaunay triangle + // (not actually using these for anything...) + //_triangles.push(new Triangle(bottomSite, topSite, rightRegion(lbnd))); + + v = lbnd.vertex; + v.setIndex(); + lbnd.edge.setVertex(lbnd.leftRight, v); + rbnd.edge.setVertex(rbnd.leftRight, v); + edgeList.remove(lbnd); + heap.remove(rbnd); + edgeList.remove(rbnd); + leftRight = LR.LEFT; + if (bottomSite.y > topSite.y) + { + tempSite = bottomSite; bottomSite = topSite; topSite = tempSite; leftRight = LR.RIGHT; + } + edge = Edge.createBisectingEdge(bottomSite, topSite); + _edges.push(edge); + bisector = Halfedge.create(edge, leftRight); + halfEdges.push(bisector); + edgeList.insert(llbnd, bisector); + edge.setVertex(LR.other(leftRight), v); + if ((vertex = Vertex.intersect(llbnd, bisector)) != null) + { + vertices.push(vertex); + heap.remove(llbnd); + llbnd.vertex = vertex; + llbnd.ystar = vertex.y + bottomSite.dist(vertex); + heap.insert(llbnd); + } + if ((vertex = Vertex.intersect(bisector, rrbnd)) != null) + { + vertices.push(vertex); + bisector.vertex = vertex; + bisector.ystar = vertex.y + bottomSite.dist(vertex); + heap.insert(bisector); + } + } + else + { + break; + } + } + + // heap should be empty now + heap.dispose(); + edgeList.dispose(); + + for each (var halfEdge:Halfedge in halfEdges) + { + halfEdge.reallyDispose(); + } + halfEdges.length = 0; + + // we need the vertices to clip the edges + for each (edge in _edges) + { + edge.clipVertices(_plotBounds); + } + + // but we don't actually ever use them again! + for each (vertex in vertices) + { + vertex.dispose(); + } + vertices.length = 0; + + function leftRegion(he:Halfedge):Site + { + var edge:Edge = he.edge; + if (!edge) + { + return bottomMostSite; + } + + return edge.site(he.leftRight); + } + + function rightRegion(he:Halfedge):Site + { + var edge:Edge = he.edge; + if (!edge) + { + return bottomMostSite; + } + + return edge.site(LR.other(he.leftRight)); + } + } + + internal static function compareByYThenX(s1:Site, s2:*):Number + { + if (s1.y < s2.y) return -1; + if (s1.y > s2.y) return 1; + if (s1.x < s2.x) return -1; + if (s1.x > s2.x) return 1; + return 0; + } + } +} diff --git a/library/src/com/nodename/Delaunay/delaunayLinesForEdges.as b/library/src/com/nodename/Delaunay/delaunayLinesForEdges.as new file mode 100644 index 0000000..bc47795 --- /dev/null +++ b/library/src/com/nodename/Delaunay/delaunayLinesForEdges.as @@ -0,0 +1,15 @@ +package com.nodename.Delaunay +{ + import com.nodename.geom.LineSegment; + + internal function delaunayLinesForEdges(edges:Vector.):Vector. + { + var segments:Vector. = new Vector.(); + for each (var edge:Edge in edges) + { + segments[segments.length] = edge.delaunayLine(); + } + + return segments; + } +} diff --git a/library/src/com/nodename/Delaunay/kruskal.as b/library/src/com/nodename/Delaunay/kruskal.as new file mode 100644 index 0000000..bb42b68 --- /dev/null +++ b/library/src/com/nodename/Delaunay/kruskal.as @@ -0,0 +1,119 @@ +package com.nodename.Delaunay +{ + import flash.utils.Dictionary; + import com.nodename.geom.LineSegment; + + /** + * Kruskal's spanning tree algorithm with union-find + * Skiena: The Algorithm Design Manual, p. 196ff + * Note: the sites are implied: they consist of the end points of the line segments + */ + public function kruskal(lineSegments:Vector., type:String = "minimum"):Vector. + { + var nodes:Dictionary = new Dictionary(true); + var mst:Vector. = new Vector.(); + var nodePool:Vector. = Node.pool; + + switch (type) + { + // note that the compare functions are the reverse of what you'd expect + // because (see below) we traverse the lineSegments in reverse order for speed + case "maximum": + lineSegments.sort(LineSegment.compareLengths); + break; + default: + lineSegments.sort(LineSegment.compareLengths_MAX); + break; + } + + for (var i:int = lineSegments.length; --i > -1;) + { + var lineSegment:LineSegment = lineSegments[i]; + + var node0:Node = nodes[lineSegment.p0]; + var rootOfSet0:Node; + if (node0 == null) + { + node0 = nodePool.length > 0 ? nodePool.pop() : new Node(); + // intialize the node: + rootOfSet0 = node0.parent = node0; + node0.treeSize = 1; + + nodes[lineSegment.p0] = node0; + } + else + { + rootOfSet0 = find(node0); + } + + var node1:Node = nodes[lineSegment.p1]; + var rootOfSet1:Node; + if (node1 == null) + { + node1 = nodePool.length > 0 ? nodePool.pop() : new Node(); + // intialize the node: + rootOfSet1 = node1.parent = node1; + node1.treeSize = 1; + + nodes[lineSegment.p1] = node1; + } + else + { + rootOfSet1 = find(node1); + } + + if (rootOfSet0 != rootOfSet1) // nodes not in same set + { + mst.push(lineSegment); + + // merge the two sets: + var treeSize0:int = rootOfSet0.treeSize; + var treeSize1:int = rootOfSet1.treeSize; + if (treeSize0 >= treeSize1) + { + // set0 absorbs set1: + rootOfSet1.parent = rootOfSet0; + rootOfSet0.treeSize += treeSize1; + } + else + { + // set1 absorbs set0: + rootOfSet0.parent = rootOfSet1; + rootOfSet1.treeSize += treeSize0; + } + } + } + + for each (var node:Node in nodes) + { + nodePool.push(node); + } + + return mst; + } +} + +function find(node:Node):Node +{ + if (node.parent == node) + { + return node; + } + else + { + var root:Node = find(node.parent); + // this line is just to speed up subsequent finds by keeping the tree depth low: + node.parent = root; + return root; + } +} + +class Node +{ + public static var pool:Vector. = new Vector.(); + + public var parent:Node; + public var treeSize:int; + + public function Node() {} +} diff --git a/library/src/com/nodename/Delaunay/selectEdgesForSitePoint.as b/library/src/com/nodename/Delaunay/selectEdgesForSitePoint.as new file mode 100644 index 0000000..467305e --- /dev/null +++ b/library/src/com/nodename/Delaunay/selectEdgesForSitePoint.as @@ -0,0 +1,15 @@ +package com.nodename.Delaunay +{ + import flash.geom.Point; + + internal function selectEdgesForSitePoint(coord:Point, edgesToTest:Vector.):Vector. + { + return edgesToTest.filter(myTest); + + function myTest(edge:Edge, index:int, vector:Vector.):Boolean + { + return ((edge.leftSite && edge.leftSite.coord == coord) + || (edge.rightSite && edge.rightSite.coord == coord)); + } + } +} diff --git a/library/src/com/nodename/Delaunay/selectNonIntersectingEdges.as b/library/src/com/nodename/Delaunay/selectNonIntersectingEdges.as new file mode 100644 index 0000000..011dcfe --- /dev/null +++ b/library/src/com/nodename/Delaunay/selectNonIntersectingEdges.as @@ -0,0 +1,24 @@ +package com.nodename.Delaunay +{ + import flash.geom.Point; + import flash.display.BitmapData; + + internal function selectNonIntersectingEdges(keepOutMask:BitmapData, edgesToTest:Vector.):Vector. + { + if (keepOutMask == null) + { + return edgesToTest; + } + + var zeroPoint:Point = new Point(); + return edgesToTest.filter(myTest); + + function myTest(edge:Edge, index:int, vector:Vector.):Boolean + { + var delaunayLineBmp:BitmapData = edge.makeDelaunayLineBmp(); + var notIntersecting:Boolean = !(keepOutMask.hitTest(zeroPoint, 1, delaunayLineBmp, zeroPoint, 1)); + delaunayLineBmp.dispose(); + return notIntersecting; + } + } +} diff --git a/library/src/com/nodename/Delaunay/visibleLineSegments.as b/library/src/com/nodename/Delaunay/visibleLineSegments.as new file mode 100644 index 0000000..099fa0f --- /dev/null +++ b/library/src/com/nodename/Delaunay/visibleLineSegments.as @@ -0,0 +1,22 @@ +package com.nodename.Delaunay +{ + import com.nodename.geom.LineSegment; + import flash.geom.Point; + + internal function visibleLineSegments(edges:Vector.):Vector. + { + var segments:Vector. = new Vector.(); + + for each (var edge:Edge in edges) + { + if (edge.visible) + { + var p1:Point = edge.clippedEnds[LR.LEFT]; + var p2:Point = edge.clippedEnds[LR.RIGHT]; + segments.push(new LineSegment(p1, p2)); + } + } + + return segments; + } +} diff --git a/library/src/com/nodename/geom/Circle.as b/library/src/com/nodename/geom/Circle.as new file mode 100644 index 0000000..a9e4bbc --- /dev/null +++ b/library/src/com/nodename/geom/Circle.as @@ -0,0 +1,22 @@ +package com.nodename.geom +{ + import flash.geom.Point; + + public final class Circle extends Object + { + public var center:Point; + public var radius:Number; + + public function Circle(centerX:Number, centerY:Number, radius:Number) + { + super(); + this.center = new Point(centerX, centerY); + this.radius = radius; + } + + public function toString():String + { + return "Circle (center: " + center + "; radius: " + radius + ")"; + } + } +} diff --git a/library/src/com/nodename/geom/LineSegment.as b/library/src/com/nodename/geom/LineSegment.as new file mode 100644 index 0000000..f75dfc8 --- /dev/null +++ b/library/src/com/nodename/geom/LineSegment.as @@ -0,0 +1,38 @@ +package com.nodename.geom +{ + import flash.geom.Point; + + public final class LineSegment extends Object + { + public static function compareLengths_MAX(segment0:LineSegment, segment1:LineSegment):Number + { + var length0:Number = Point.distance(segment0.p0, segment0.p1); + var length1:Number = Point.distance(segment1.p0, segment1.p1); + if (length0 < length1) + { + return 1; + } + if (length0 > length1) + { + return -1; + } + return 0; + } + + public static function compareLengths(edge0:LineSegment, edge1:LineSegment):Number + { + return - compareLengths_MAX(edge0, edge1); + } + + public var p0:Point; + public var p1:Point; + + public function LineSegment(p0:Point, p1:Point) + { + super(); + this.p0 = p0; + this.p1 = p1; + } + + } +} diff --git a/library/src/com/nodename/geom/Polygon.as b/library/src/com/nodename/geom/Polygon.as new file mode 100644 index 0000000..1781f54 --- /dev/null +++ b/library/src/com/nodename/geom/Polygon.as @@ -0,0 +1,51 @@ +package com.nodename.geom +{ + import flash.geom.Point; + + public final class Polygon + { + private var m_vertices:Vector.; + + public function Polygon(vertices:Vector.) + { + m_vertices = vertices; + } + + public function area():Number + { + return Math.abs(signedDoubleArea() * 0.5); + } + + public function winding():String + { + var signedDoubleArea:Number = this.signedDoubleArea(); + if (signedDoubleArea < 0) + { + return Winding.CLOCKWISE; + } + + if (signedDoubleArea > 0) + { + return Winding.COUNTERCLOCKWISE; + } + + return Winding.NONE; + } + + private function signedDoubleArea():Number + { + var index:uint, nextIndex:uint; + var n:uint = m_vertices.length; + var point:Point, next:Point; + var signedDoubleArea:Number = 0; + for (index = 0; index < n; ++index) + { + nextIndex = (index + 1) % n; + point = m_vertices[index] as Point; + next = m_vertices[nextIndex] as Point; + signedDoubleArea += point.x * next.y - next.x * point.y; + } + return signedDoubleArea; + } + } +} diff --git a/library/src/com/nodename/geom/Winding.as b/library/src/com/nodename/geom/Winding.as new file mode 100644 index 0000000..fa4cccd --- /dev/null +++ b/library/src/com/nodename/geom/Winding.as @@ -0,0 +1,24 @@ +package com.nodename.geom +{ + import com.mignari.errors.AbstractClassError; + + public final class Winding + { + //-------------------------------------------------------------------------- + // CONSTRUCTOR + //-------------------------------------------------------------------------- + + public function Winding() + { + throw new AbstractClassError(Winding); + } + + //-------------------------------------------------------------------------- + // STATIC + //-------------------------------------------------------------------------- + + static public const NONE:String = "none"; + static public const CLOCKWISE:String = "clockwise"; + static public const COUNTERCLOCKWISE:String = "counterclockwise" + } +} diff --git a/library/src/com/nodename/utils/IDisposable.as b/library/src/com/nodename/utils/IDisposable.as new file mode 100644 index 0000000..19ef634 --- /dev/null +++ b/library/src/com/nodename/utils/IDisposable.as @@ -0,0 +1,7 @@ +package com.nodename.utils +{ + public interface IDisposable + { + function dispose():void; + } +} diff --git a/library/src/de/polygonal/math/PM_PRNG.as b/library/src/de/polygonal/math/PM_PRNG.as new file mode 100644 index 0000000..6359e82 --- /dev/null +++ b/library/src/de/polygonal/math/PM_PRNG.as @@ -0,0 +1,116 @@ +/* +* Copyright (c) 2009 Michael Baczynski, http://www.polygonal.de +* +* Permission is hereby granted, free of charge, to any person obtaining +* a copy of this software and associated documentation files (the +* "Software"), to deal in the Software without restriction, including +* without limitation the rights to use, copy, modify, merge, publish, +* distribute, sublicense, and/or sell copies of the Software, and to +* permit persons to whom the Software is furnished to do so, subject to +* the following conditions: +* The above copyright notice and this permission notice shall be +* included in all copies or substantial portions of the Software. +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/** + * Implementation of the Park Miller (1988) "minimal standard" linear + * congruential pseudo-random number generator. + * + * For a full explanation visit: http://www.firstpr.com.au/dsp/rand31/ + * + * The generator uses a modulus constant (m) of 2^31 - 1 which is a + * Mersenne Prime number and a full-period-multiplier of 16807. + * Output is a 31 bit unsigned integer. The range of values output is + * 1 to 2,147,483,646 (2^31-1) and the seed must be in this range too. + * + * David G. Carta's optimisation which needs only 32 bit integer math, + * and no division is actually *slower* in flash (both AS2 & AS3) so + * it's better to use the double-precision floating point version. + * + * @author Michael Baczynski, www.polygonal.de + */ +package de.polygonal.math +{ + public class PM_PRNG + { + /** + * set seed with a 31 bit unsigned integer + * between 1 and 0X7FFFFFFE inclusive. don't use 0! + */ + public var seed:uint; + + public function PM_PRNG() + { + seed = 1; + } + + /** + * provides the next pseudorandom number + * as an unsigned integer (31 bits) + */ + public function nextInt():uint + { + return gen(); + } + + /** + * provides the next pseudorandom number + * as a float between nearly 0 and nearly 1.0. + */ + public function nextDouble():Number + { + return (gen() / 2147483647); + } + + /** + * provides the next pseudorandom number + * as an unsigned integer (31 bits) betweeen + * a given range. + */ + public function nextIntRange(min:Number, max:Number):uint + { + min -= .4999; + max += .4999; + return Math.round(min + ((max - min) * nextDouble())); + } + + /** + * provides the next pseudorandom number + * as a float between a given range. + */ + public function nextDoubleRange(min:Number, max:Number):Number + { + return min + ((max - min) * nextDouble()); + } + + /** + * generator: + * new-value = (old-value * 16807) mod (2^31 - 1) + */ + private function gen():uint + { + //integer version 1, for max int 2^46 - 1 or larger. + return seed = (seed * 16807) % 2147483647; + + /** + * integer version 2, for max int 2^31 - 1 (slowest) + */ + //var test:int = 16807 * (seed % 127773 >> 0) - 2836 * (seed / 127773 >> 0); + //return seed = (test > 0 ? test : test + 2147483647); + + /** + * david g. carta's optimisation is 15% slower than integer version 1 + */ + //var hi:uint = 16807 * (seed >> 16); + //var lo:uint = 16807 * (seed & 0xFFFF) + ((hi & 0x7FFF) << 16) + (hi >> 15); + //return seed = (lo > 0x7FFFFFFF ? lo - 0x7FFFFFFF : lo); + } + } +}