├── .gitattributes ├── .gitignore ├── BepInEx ├── BepInEx.csproj ├── Plugin.cs ├── icon.png └── manifest.json ├── CHANGELOG.md ├── Code ├── LineModes │ ├── Circle.cs │ ├── LineBase.cs │ ├── LineMode.cs │ ├── PointData.cs │ ├── SimpleCurve.cs │ ├── SpacingMode.cs │ └── StraightLine.cs ├── Localization.cs ├── Mod.cs └── Systems │ ├── LineToolSystem.cs │ ├── LineToolTooltipSystem.cs │ └── LineToolUISystem.cs ├── Config ├── PostBuild.csproj ├── References.csproj └── Targets.csproj ├── GlobalSuppressions.cs ├── Icons ├── Circle.svg ├── Dice.svg ├── Fence.svg └── MeasureEven.svg ├── LICENSE.txt ├── LineToolLite.csproj ├── LineToolLite.sln ├── NOTICE.txt ├── README.md ├── UI ├── common.js ├── modes.html ├── modes.js ├── ui.css ├── ui.html └── ui.js ├── l10n.csv └── stylecop.json /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /BepInEx/BepInEx.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | https://nuget.bepinex.dev/v3/index.json; 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /BepInEx/Plugin.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | namespace LineTool 8 | { 9 | using System.Reflection; 10 | using BepInEx; 11 | using Game; 12 | using Game.Common; 13 | using Game.SceneFlow; 14 | using HarmonyLib; 15 | 16 | /// 17 | /// BepInEx plugin to substitute for IMod support. 18 | /// 19 | [BepInPlugin(GUID, "Line Tool Lite", "1.3.4")] 20 | [HarmonyPatch] 21 | public class Plugin : BaseUnityPlugin 22 | { 23 | /// 24 | /// Plugin unique GUID. 25 | /// 26 | public const string GUID = "com.github.algernon-A.CS2.LineToolLite"; 27 | 28 | // IMod instance reference. 29 | private Mod _mod; 30 | 31 | /// 32 | /// Called when the plugin is loaded. 33 | /// 34 | public void Awake() 35 | { 36 | // Ersatz IMod.OnLoad(). 37 | _mod = new (); 38 | _mod.OnLoad(); 39 | 40 | _mod.Log.Info("Plugin.Awake"); 41 | 42 | // Apply Harmony patches. 43 | Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), GUID); 44 | } 45 | 46 | /// 47 | /// Harmony postfix to to substitute for IMod.OnCreateWorld. 48 | /// 49 | /// instance. 50 | [HarmonyPatch(typeof(SystemOrder), nameof(SystemOrder.Initialize))] 51 | [HarmonyPostfix] 52 | private static void InjectSystems(UpdateSystem updateSystem) => Mod.Instance.OnCreateWorld(updateSystem); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /BepInEx/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algernon-A/LineToolLite/39b29401825fe8d52f62f9e25df3c128670f2bba/BepInEx/icon.png -------------------------------------------------------------------------------- /BepInEx/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Line_Tool_Lite", 3 | "version_number": "1.3.4", 4 | "website_url": "https://github.com/algernon-A/LineToolLite", 5 | "description": "Place objects in lines, curves, or circles. A variety of options and controls are availalbe to specify and fine-tune results.", 6 | "dependencies": [ 7 | "BepInEx-BepInExPack-5.4.2100" 8 | ] 9 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.4 2 | - Add mouse scroll wheel adjustments to numerical fields. 3 | - Fix missing localization for modes panel title. 4 | - Add Traditional Chinese localization from CrowdIn translation volunteers. 5 | - Adjust starting position for curves when using fence mode. 6 | 7 | ## 1.3.3 8 | - Ensure new trees have default growth state. 9 | 10 | ## 1.3.2 11 | - Fix XP being gained when previewing some buildings and roundabouts. 12 | 13 | ## 1.3.1 14 | - Update README with tool mode location image. 15 | 16 | ## 1.3 17 | - Add integration with existing tool panels (no more hotkey). 18 | - Add initial localizations for Simplified Chinese, German, and Spanish from CrowdIn translation volunteers. 19 | 20 | ### 1.2.1 21 | - Add explicit frame sync to tooltips. 22 | 23 | ## 1.2 24 | - Add tooltips to tool panel. 25 | - Add localization. 26 | - Add proper licensing. 27 | 28 | ### 1.1.2 29 | - Additional updates for Tree Controller integration. 30 | 31 | ### 1.1.1 32 | - Update Tree Controller integration. 33 | 34 | ## 1.1 35 | - Add dragging of line control points in fixed preview mode. 36 | 37 | ### 1.0.10 38 | - Add fixed-length even spacing mode (will evenly space out objects along the full length of the line with spacing as close as possible to the specified distance). 39 | 40 | ### 1.0.9 41 | - Continuing a curve with shift-click now also locks the starting tangent of the new curve (continuous curves). 42 | 43 | ### 1.0.8 44 | - Add UI in editor. 45 | 46 | ### 1.0.7 47 | - Improve previewing and cursor handling (again). 48 | 49 | ### 1.0.6 50 | - Add optional random spacing and lateral offset functions. 51 | 52 | ### 1.0.5 53 | - Enforce minimum spacing distance to prevent overlapping of multiple invisible items. 54 | 55 | ### 1.0.4 56 | - Improve previewing and cursor handling. 57 | - Fix spacing setting not being reset after re-activating tool when fence mode is set. 58 | 59 | ### 1.0.3 60 | - Add initial fence mode. 61 | 62 | ### 1.0.2 63 | - Rework UI JavaScript to survive UI regeneration and enable compatibility with HookUI (thanks to Captain of Coit). 64 | - Remove JavaScript globals. 65 | 66 | ### 1.0.1 67 | - Embed icons in mod instead of using Unified Icon Library due to Thunderstore mod manager breaking things. 68 | - Add warning about HookUI incompatibility. -------------------------------------------------------------------------------- /Code/LineModes/Circle.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | namespace LineTool 8 | { 9 | using Colossal.Mathematics; 10 | using Game.Simulation; 11 | using Unity.Collections; 12 | using Unity.Mathematics; 13 | 14 | /// 15 | /// Circle placement mode. 16 | /// 17 | public class Circle : LineBase 18 | { 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// Mode to copy starting state from. 23 | public Circle(LineBase mode) 24 | : base(mode) 25 | { 26 | } 27 | 28 | /// 29 | /// Calculates the points to use based on this mode. 30 | /// 31 | /// Selection current position. 32 | /// Active spacing mode. 33 | /// Spacing distance. 34 | /// Random spacing offset maximum. 35 | /// Random lateral offset maximum. 36 | /// Rotation setting. 37 | /// Prefab zBounds. 38 | /// List of points to populate. 39 | /// Terrain height data reference. 40 | public override void CalculatePoints(float3 currentPos, SpacingMode spacingMode, float spacing, float randomSpacing, float randomOffset, int rotation, Bounds1 zBounds, NativeList pointList, ref TerrainHeightData heightData) 41 | { 42 | // Don't do anything if we don't have valid start. 43 | if (!m_validStart) 44 | { 45 | return; 46 | } 47 | 48 | // Calculate length. 49 | float3 difference = currentPos - m_startPos; 50 | float radius = math.length(difference); 51 | 52 | // Calculate spacing. 53 | float circumference = radius * math.PI * 2f; 54 | float numPoints = spacingMode == SpacingMode.FullLength ? math.round(circumference / spacing) : math.floor(circumference / spacing); 55 | float increment = (math.PI * 2f) / numPoints; 56 | float startAngle = math.atan2(difference.z, difference.x); 57 | System.Random random = new ((int)circumference * 1000); 58 | 59 | // Create points. 60 | for (float i = startAngle; i < startAngle + (math.PI * 2f); i += increment) 61 | { 62 | // Apply spacing adjustment. 63 | float adjustedAngle = i; 64 | if (randomSpacing > 0f && spacingMode != SpacingMode.FenceMode) 65 | { 66 | float distanceAdjustment = (float)(random.NextDouble() * randomSpacing * 2f) - randomSpacing; 67 | adjustedAngle += (distanceAdjustment * math.PI * 2f) / circumference; 68 | } 69 | 70 | // Calculate point. 71 | float xPos = radius * math.cos(adjustedAngle); 72 | float yPos = radius * math.sin(adjustedAngle); 73 | float3 thisPoint = new (m_startPos.x + xPos, m_startPos.y, m_startPos.z + yPos); 74 | 75 | // Apply offset adjustment. 76 | if (randomOffset > 0f && spacingMode != SpacingMode.FenceMode) 77 | { 78 | thisPoint += math.normalize(thisPoint - m_startPos) * ((float)(randomOffset * random.NextDouble() * 2f) - randomOffset); 79 | } 80 | 81 | // Calculate terrain height. 82 | thisPoint.y = TerrainUtils.SampleHeight(ref heightData, thisPoint); 83 | 84 | // Add point to list. 85 | pointList.Add(new PointData { Position = thisPoint, Rotation = quaternion.Euler(0f, math.radians(rotation) - i, 0f), }); 86 | } 87 | 88 | // Record end position for overlays. 89 | m_endPos = currentPos; 90 | } 91 | 92 | /// 93 | /// Performs actions after items are placed on the current line, setting up for the next line to be set. 94 | /// 95 | /// Click world location. 96 | public override void ItemsPlaced(float3 location) 97 | { 98 | // Empty, to retain original start position (centre of circle). 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Code/LineModes/LineBase.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | namespace LineTool 8 | { 9 | using Colossal.Mathematics; 10 | using Game.Rendering; 11 | using Game.Simulation; 12 | using Unity.Collections; 13 | using Unity.Mathematics; 14 | using UnityEngine; 15 | using static Game.Rendering.GuideLinesSystem; 16 | using static LineToolSystem; 17 | 18 | /// 19 | /// Line placement mode. 20 | /// 21 | [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Protected fields")] 22 | public abstract class LineBase 23 | { 24 | /// 25 | /// Selection radius of points. 26 | /// 27 | protected const float PointRadius = 8f; 28 | 29 | /// 30 | /// Indicates whether a valid starting position has been recorded. 31 | /// 32 | protected bool m_validStart; 33 | 34 | /// 35 | /// Records the current selection start position. 36 | /// 37 | protected float3 m_startPos; 38 | 39 | /// 40 | /// Records the current selection end position. 41 | /// 42 | protected float3 m_endPos; 43 | 44 | /// 45 | /// Initializes a new instance of the class. 46 | /// 47 | public LineBase() 48 | { 49 | // Basic state. 50 | m_validStart = false; 51 | } 52 | 53 | /// 54 | /// Initializes a new instance of the class. 55 | /// 56 | /// Mode to copy starting state from. 57 | public LineBase(LineBase mode) 58 | { 59 | m_validStart = mode.m_validStart; 60 | m_startPos = mode.m_startPos; 61 | } 62 | 63 | /// 64 | /// Gets a value indicating whether a valid starting position has been recorded. 65 | /// 66 | public bool HasStart => m_validStart; 67 | 68 | /// 69 | /// Gets a value indicating whether we're ready to place (we have enough control positions). 70 | /// 71 | public virtual bool HasAllPoints => m_validStart; 72 | 73 | /// 74 | /// Handles a mouse click. 75 | /// 76 | /// Click world position. 77 | /// true if items are to be placed as a result of this click, false otherwise. 78 | public virtual bool HandleClick(float3 position) 79 | { 80 | // If no valid start position is set, record it. 81 | if (!m_validStart) 82 | { 83 | m_startPos = position; 84 | m_endPos = position; 85 | m_validStart = true; 86 | 87 | // No placement at this stage (only the first click has been made). 88 | return false; 89 | } 90 | 91 | // Second click; we're placing items. 92 | return true; 93 | } 94 | 95 | /// 96 | /// Performs actions after items are placed on the current line, setting up for the next line to be set. 97 | /// 98 | /// Click world position. 99 | public virtual void ItemsPlaced(float3 position) 100 | { 101 | // Update new starting location to the previous end point. 102 | m_startPos = position; 103 | } 104 | 105 | /// 106 | /// Calculates the points to use based on this mode. 107 | /// 108 | /// Selection current position. 109 | /// Active spacing mode. 110 | /// Spacing distance. 111 | /// Random spacing offset maximum. 112 | /// Random lateral offset maximum. 113 | /// Rotation setting. 114 | /// Prefab zBounds. 115 | /// List of points to populate. 116 | /// Terrain height data reference. 117 | public virtual void CalculatePoints(float3 currentPos, SpacingMode spacingMode, float spacing, float randomSpacing, float randomOffset, int rotation, Bounds1 zBounds, NativeList pointList, ref TerrainHeightData heightData) 118 | { 119 | // Don't do anything if we don't have a valid start point. 120 | if (!m_validStart) 121 | { 122 | return; 123 | } 124 | 125 | // Calculate length. 126 | float3 difference = currentPos - m_startPos; 127 | float length = math.length(difference); 128 | System.Random random = new ((int)length * 1000); 129 | 130 | // Calculate applied rotation (in radians). 131 | float appliedRotation = math.radians(rotation); 132 | if (spacingMode == SpacingMode.FenceMode) 133 | { 134 | appliedRotation = math.atan2(difference.x, difference.z); 135 | } 136 | 137 | // Rotation quaternion. 138 | quaternion qRotation = quaternion.Euler(0f, appliedRotation, 0f); 139 | 140 | // Calculate even full-length spacing if needed. 141 | float adjustedSpacing = spacing; 142 | if (spacingMode == SpacingMode.FullLength) 143 | { 144 | adjustedSpacing = length / math.round(length / spacing); 145 | } 146 | 147 | // Create points. 148 | float currentDistance = spacingMode == SpacingMode.FenceMode ? -zBounds.min : 0f; 149 | float endLength = spacingMode == SpacingMode.FenceMode ? length - zBounds.max : length; 150 | while (currentDistance < endLength) 151 | { 152 | // Calculate interpolated point. 153 | float spacingAdjustment = 0f; 154 | if (randomSpacing > 0f && spacingMode != SpacingMode.FenceMode) 155 | { 156 | spacingAdjustment = (float)(random.NextDouble() * randomSpacing * 2f) - randomSpacing; 157 | } 158 | 159 | float3 thisPoint = math.lerp(m_startPos, currentPos, (currentDistance + spacingAdjustment) / length); 160 | 161 | // Apply offset adjustment. 162 | if (randomOffset > 0f && spacingMode != SpacingMode.FenceMode) 163 | { 164 | float3 left = math.normalize(new float3(-difference.z, 0f, difference.x)); 165 | thisPoint += left * ((float)(randomOffset * random.NextDouble() * 2f) - randomOffset); 166 | } 167 | 168 | thisPoint.y = TerrainUtils.SampleHeight(ref heightData, thisPoint); 169 | 170 | // Add point to list. 171 | pointList.Add(new PointData { Position = thisPoint, Rotation = qRotation, }); 172 | currentDistance += adjustedSpacing; 173 | } 174 | 175 | // Final item for full-length mode if required (if there was a distance overshoot). 176 | if (spacingMode == SpacingMode.FullLength && currentDistance < length + adjustedSpacing) 177 | { 178 | float3 thisPoint = currentPos; 179 | thisPoint.y = TerrainUtils.SampleHeight(ref heightData, thisPoint); 180 | 181 | // Add point to list. 182 | pointList.Add(new PointData { Position = thisPoint, Rotation = qRotation, }); 183 | } 184 | 185 | // Record end position for overlays. 186 | m_endPos = currentPos; 187 | } 188 | 189 | /// 190 | /// Draws any applicable overlay. 191 | /// 192 | /// Overlay buffer. 193 | /// Tooltip list. 194 | public virtual void DrawOverlay(OverlayRenderSystem.Buffer overlayBuffer, NativeList tooltips) 195 | { 196 | // Don't draw overlay if we don't have a valid start. 197 | if (m_validStart) 198 | { 199 | DrawDashedLine(m_startPos, m_endPos, new Line3.Segment(m_startPos, m_endPos), overlayBuffer, tooltips); 200 | } 201 | } 202 | 203 | /// 204 | /// Draws point overlays. 205 | /// 206 | /// Overlay buffer. 207 | public virtual void DrawPointOverlays(OverlayRenderSystem.Buffer overlayBuffer) 208 | { 209 | Color softCyan = Color.cyan; 210 | softCyan.a *= 0.1f; 211 | 212 | overlayBuffer.DrawCircle(Color.cyan, softCyan, 0.3f, 0, new float2(0f, 1f), m_startPos, PointRadius * 2f); 213 | overlayBuffer.DrawCircle(Color.cyan, softCyan, 0.3f, 0, new float2(0f, 1f), m_endPos, PointRadius * 2f); 214 | } 215 | 216 | /// 217 | /// Clears the current selection. 218 | /// 219 | public virtual void Reset() 220 | { 221 | m_validStart = false; 222 | } 223 | 224 | /// 225 | /// Checks to see if a click should initiate point dragging. 226 | /// 227 | /// Click position in world space. 228 | /// Drag mode. 229 | internal virtual DragMode CheckDragHit(float3 position) 230 | { 231 | if (math.distancesq(position, m_startPos) < (PointRadius * PointRadius)) 232 | { 233 | // Start point. 234 | return DragMode.StartPos; 235 | } 236 | else if (math.distancesq(position, m_endPos) < (PointRadius * PointRadius)) 237 | { 238 | // End point. 239 | return DragMode.EndPos; 240 | } 241 | 242 | // No hit. 243 | return DragMode.None; 244 | } 245 | 246 | /// 247 | /// Handles dragging action. 248 | /// 249 | /// Dragging mode. 250 | /// New position. 251 | internal virtual void HandleDrag(DragMode dragMode, float3 position) 252 | { 253 | // Drag start point. 254 | if (dragMode == DragMode.StartPos) 255 | { 256 | m_startPos = position; 257 | } 258 | } 259 | 260 | /// 261 | /// Draws a dashed line overlay between the two given points. 262 | /// 263 | /// Line start position. 264 | /// Line end position. 265 | /// Line segment. 266 | /// Overlay buffer. 267 | /// Tooltip list. 268 | protected void DrawDashedLine(float3 startPos, float3 endPos, Line3.Segment segment, OverlayRenderSystem.Buffer overlayBuffer, NativeList tooltips) 269 | { 270 | const float LineWidth = 1f; 271 | 272 | float distance = math.distance(startPos.xz, endPos.xz); 273 | 274 | // Don't draw lines for short distances. 275 | if (distance > LineWidth * 8f) 276 | { 277 | // Offset segment, mimicking game simple curve overlay, to ensure dash spacing. 278 | float3 offset = (segment.b - segment.a) * (LineWidth * 4f / distance); 279 | Line3.Segment line = new (segment.a + offset, segment.b - offset); 280 | 281 | // Draw line - distance figures mimic game simple curve overlay. 282 | overlayBuffer.DrawDashedLine(Color.white, line, LineWidth * 3f, LineWidth * 5f, LineWidth * 3f); 283 | 284 | // Add length tooltip. 285 | int length = Mathf.RoundToInt(math.distance(startPos.xz, endPos.xz)); 286 | if (length > 0) 287 | { 288 | tooltips.Add(new TooltipInfo(TooltipType.Length, (startPos + endPos) * 0.5f, length)); 289 | } 290 | } 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /Code/LineModes/LineMode.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | namespace LineTool 8 | { 9 | /// 10 | /// Line tool modes. 11 | /// 12 | public enum LineMode 13 | { 14 | /// 15 | /// Straight line. 16 | /// 17 | Straight, 18 | 19 | /// 20 | /// Simple curve. 21 | /// 22 | SimpleCurve, 23 | 24 | /// 25 | /// Circle. 26 | /// 27 | Circle, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Code/LineModes/PointData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | namespace LineTool 8 | { 9 | using Unity.Mathematics; 10 | 11 | /// 12 | /// Data struct for calculated point. 13 | /// 14 | public struct PointData 15 | { 16 | /// 17 | /// Point location. 18 | /// 19 | public float3 Position; 20 | 21 | /// 22 | /// Point rotation. 23 | /// 24 | public quaternion Rotation; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Code/LineModes/SimpleCurve.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | namespace LineTool 8 | { 9 | using Colossal.Mathematics; 10 | using Game.Net; 11 | using Game.Rendering; 12 | using Game.Simulation; 13 | using Unity.Collections; 14 | using Unity.Mathematics; 15 | using UnityEngine; 16 | using static Game.Rendering.GuideLinesSystem; 17 | using static LineToolSystem; 18 | 19 | /// 20 | /// Simple curve placement mode. 21 | /// 22 | public class SimpleCurve : LineBase 23 | { 24 | // Current elbow point. 25 | private bool _validElbow = false; 26 | private bool _validPreviousElbow = false; 27 | private float3 _elbowPoint; 28 | private float3 _previousElbowPoint; 29 | 30 | // Calculated Bezier. 31 | private Bezier4x3 _thisBezier; 32 | 33 | /// 34 | /// Initializes a new instance of the class. 35 | /// 36 | /// Mode to copy starting state from. 37 | public SimpleCurve(LineBase mode) 38 | : base(mode) 39 | { 40 | } 41 | 42 | /// 43 | /// Gets a value indicating whether we're ready to place (we have enough control positions). 44 | /// 45 | public override bool HasAllPoints => m_validStart & _validElbow; 46 | 47 | /// 48 | /// Handles a mouse click. 49 | /// 50 | /// Click world position. 51 | /// True if items are to be placed as a result of this click, false otherwise. 52 | public override bool HandleClick(float3 position) 53 | { 54 | // If no valid initial point, record this as the first point. 55 | if (!m_validStart) 56 | { 57 | m_startPos = position; 58 | m_endPos = position; 59 | m_validStart = true; 60 | return false; 61 | } 62 | 63 | // Otherwise, if no valid elbow point, record this as the elbow point. 64 | if (!_validElbow) 65 | { 66 | _elbowPoint = ConstrainPos(position); 67 | _validElbow = true; 68 | return false; 69 | } 70 | 71 | // Place the items on the curve. 72 | return true; 73 | } 74 | 75 | /// 76 | /// Performs actions after items are placed on the current line, setting up for the next line to be set. 77 | /// 78 | /// Click world position. 79 | public override void ItemsPlaced(float3 position) 80 | { 81 | // Update new starting location to the previous end point and clear elbow. 82 | m_startPos = position; 83 | _validElbow = false; 84 | _previousElbowPoint = _elbowPoint; 85 | _validPreviousElbow = true; 86 | } 87 | 88 | /// 89 | /// Calculates the points to use based on this mode. 90 | /// 91 | /// Selection current position. 92 | /// Active spacing mode. 93 | /// Spacing distance. 94 | /// Random spacing offset maximum. 95 | /// Random lateral offset maximum. 96 | /// Rotation setting. 97 | /// Prefab zBounds. 98 | /// List of points to populate. 99 | /// Terrain height data reference. 100 | public override void CalculatePoints(float3 currentPos, SpacingMode spacingMode, float spacing, float randomSpacing, float randomOffset, int rotation, Bounds1 zBounds, NativeList pointList, ref TerrainHeightData heightData) 101 | { 102 | // Don't do anything if we don't have valid start. 103 | if (!m_validStart) 104 | { 105 | return; 106 | } 107 | 108 | // If we have a valid start but no valid elbow, just draw a straight line. 109 | if (!_validElbow) 110 | { 111 | // Constrain as required. 112 | m_endPos = ConstrainPos(currentPos); 113 | base.CalculatePoints(m_endPos, spacingMode, spacing, randomSpacing, randomOffset, rotation, zBounds, pointList, ref heightData); 114 | return; 115 | } 116 | 117 | // Calculate Bezier. 118 | _thisBezier = NetUtils.FitCurve(new Line3.Segment(m_startPos, _elbowPoint), new Line3.Segment(currentPos, _elbowPoint)); 119 | 120 | // Calculate even full-length spacing if needed. 121 | float adjustedSpacing = spacing; 122 | float length = MathUtils.Length(_thisBezier); 123 | if (spacingMode == SpacingMode.FullLength) 124 | { 125 | adjustedSpacing = length / math.round(length / spacing); 126 | } 127 | 128 | // Default rotation quaternion. 129 | quaternion qRotation = quaternion.Euler(0f, math.radians(rotation), 0f); 130 | 131 | // Randomizer. 132 | System.Random random = new ((int)(currentPos.x + currentPos.z) * 1000); 133 | 134 | // For fence mode offset initial spacing by object half-length (so start of item aligns with the line start point). 135 | float tFactor = spacingMode == SpacingMode.FenceMode ? BezierStep(0, spacing / 2f) : 0f; 136 | float distanceTravelled = 0f; 137 | while (tFactor < 1.0f) 138 | { 139 | // Apply spacing randomization. 140 | float adjustedT = tFactor; 141 | if (randomSpacing > 0f && spacingMode != SpacingMode.FenceMode) 142 | { 143 | float spacingAdjustment = (float)(random.NextDouble() * randomSpacing * 2f) - randomSpacing; 144 | adjustedT = spacingAdjustment < 0f ? BezierStepReverse(tFactor, spacingAdjustment) : BezierStep(tFactor, spacingAdjustment); 145 | } 146 | 147 | // Calculate point. 148 | float3 thisPoint = MathUtils.Position(_thisBezier, adjustedT); 149 | 150 | // Apply offset randomization. 151 | if (randomOffset > 0f && spacingMode != SpacingMode.FenceMode) 152 | { 153 | float3 tangent = MathUtils.Tangent(_thisBezier, adjustedT); 154 | float3 left = math.normalize(new float3(-tangent.z, 0f, tangent.x)); 155 | thisPoint += left * ((float)(randomOffset * random.NextDouble() * 2f) - randomOffset); 156 | } 157 | 158 | // Get next t factor. 159 | tFactor = BezierStep(tFactor, adjustedSpacing); 160 | distanceTravelled += adjustedSpacing; 161 | 162 | // Calculate applied rotation for fence mode. 163 | if (spacingMode == SpacingMode.FenceMode) 164 | { 165 | float3 difference = MathUtils.Position(_thisBezier, tFactor) - thisPoint; 166 | qRotation = quaternion.Euler(0f, math.atan2(difference.x, difference.z), 0f); 167 | } 168 | 169 | // Calculate terrain height. 170 | thisPoint.y = TerrainUtils.SampleHeight(ref heightData, thisPoint); 171 | 172 | // Add point to list. 173 | pointList.Add(new PointData { Position = thisPoint, Rotation = qRotation, }); 174 | } 175 | 176 | // Final item for full-length mode if required (if there was a distance overshoot). 177 | if (spacingMode == SpacingMode.FullLength && distanceTravelled < length + adjustedSpacing) 178 | { 179 | float3 thisPoint = currentPos; 180 | thisPoint.y = TerrainUtils.SampleHeight(ref heightData, thisPoint); 181 | 182 | // Add point to list. 183 | pointList.Add(new PointData { Position = thisPoint, Rotation = qRotation, }); 184 | } 185 | 186 | // Record end position for overlays. 187 | m_endPos = currentPos; 188 | } 189 | 190 | /// 191 | /// Draws any applicable overlay. 192 | /// 193 | /// Overlay buffer. 194 | /// Tooltip list. 195 | public override void DrawOverlay(OverlayRenderSystem.Buffer overlayBuffer, NativeList tooltips) 196 | { 197 | if (m_validStart) 198 | { 199 | // Draw an elbow overlay if we've got valid starting and elbow positions. 200 | if (_validElbow) 201 | { 202 | // Calculate lines. 203 | Line3.Segment line1 = new (m_startPos, _elbowPoint); 204 | Line3.Segment line2 = new (_elbowPoint, m_endPos); 205 | 206 | // Draw lines. 207 | DrawDashedLine(m_startPos, _elbowPoint, line1, overlayBuffer, tooltips); 208 | DrawDashedLine(_elbowPoint, m_endPos, line2, overlayBuffer, tooltips); 209 | 210 | // Draw angle. 211 | DrawAngleIndicator(line1, line2, 8f, 8f, overlayBuffer, tooltips); 212 | } 213 | else 214 | { 215 | // Initial position only; just draw a straight line (constrained if required). 216 | base.DrawOverlay(overlayBuffer, tooltips); 217 | } 218 | } 219 | } 220 | 221 | /// 222 | /// Draws point overlays. 223 | /// 224 | /// Overlay buffer. 225 | public override void DrawPointOverlays(OverlayRenderSystem.Buffer overlayBuffer) 226 | { 227 | base.DrawPointOverlays(overlayBuffer); 228 | 229 | // Draw elbow point. 230 | if (_validElbow) 231 | { 232 | Color softCyan = Color.cyan; 233 | softCyan.a *= 0.1f; 234 | overlayBuffer.DrawCircle(Color.cyan, softCyan, 0.3f, 0, new float2(0f, 1f), _elbowPoint, PointRadius * 2f); 235 | } 236 | } 237 | 238 | /// 239 | /// Clears the current selection. 240 | /// 241 | public override void Reset() 242 | { 243 | // Only clear elbow, if we have one. 244 | if (_validElbow) 245 | { 246 | _validElbow = false; 247 | } 248 | else 249 | { 250 | // Otherwise, reset entire state. 251 | _validPreviousElbow = false; 252 | base.Reset(); 253 | } 254 | } 255 | 256 | /// 257 | /// Checks to see if a click should initiate point dragging. 258 | /// 259 | /// Click position in world space. 260 | /// Drag mode. 261 | internal override DragMode CheckDragHit(float3 position) 262 | { 263 | // Start and end points. 264 | DragMode mode = base.CheckDragHit(position); 265 | 266 | // If no hit from base (start and end points), check for elbow point hit. 267 | if (mode == DragMode.None && _validElbow && math.distancesq(position, _elbowPoint) < (PointRadius * PointRadius)) 268 | { 269 | return DragMode.ElbowPos; 270 | } 271 | 272 | return mode; 273 | } 274 | 275 | /// 276 | /// Handles dragging action. 277 | /// 278 | /// Dragging mode. 279 | /// New position. 280 | internal override void HandleDrag(DragMode dragMode, float3 position) 281 | { 282 | if (dragMode == DragMode.ElbowPos) 283 | { 284 | // Update elbow point. 285 | _elbowPoint = position; 286 | } 287 | else 288 | { 289 | // Other points. 290 | base.HandleDrag(dragMode, position); 291 | } 292 | } 293 | 294 | /// 295 | /// Applies any active constraints the given current cursor world position. 296 | /// 297 | /// Current cursor world position. 298 | /// Constrained cursor world position. 299 | private float3 ConstrainPos(float3 currentPos) 300 | { 301 | // Constrain to continuous curve. 302 | if (m_validStart && !_validElbow && _validPreviousElbow) 303 | { 304 | // Use closest point on infinite line projected from previous curve end tangent. 305 | return math.project(currentPos - _previousElbowPoint, m_startPos - _previousElbowPoint) + _previousElbowPoint; 306 | } 307 | 308 | return currentPos; 309 | } 310 | 311 | /// 312 | /// Steps along a Bezier calculating the target t factor for the given starting t factor and the given distance. 313 | /// Code based on Alterran's PropLineTool (StepDistanceCurve, Utilities/PLTMath.cs). 314 | /// 315 | /// Starting t factor. 316 | /// Distance to travel. 317 | /// Target t factor. 318 | private float BezierStep(float tStart, float distance) 319 | { 320 | const float Tolerance = 0.001f; 321 | const float ToleranceSquared = Tolerance * Tolerance; 322 | 323 | float tEnd = Travel(tStart, distance); 324 | float usedDistance = CubicBezierArcLengthXZGauss04(tStart, tEnd); 325 | 326 | // Twelve iteration maximum for performance and to prevent infinite loops. 327 | for (int i = 0; i < 12; ++i) 328 | { 329 | // Stop looping if the remaining distance is less than tolerance. 330 | float remainingDistance = distance - usedDistance; 331 | if (remainingDistance * remainingDistance < ToleranceSquared) 332 | { 333 | break; 334 | } 335 | 336 | usedDistance = CubicBezierArcLengthXZGauss04(tStart, tEnd); 337 | tEnd += (distance - usedDistance) / CubicSpeedXZ(tEnd); 338 | } 339 | 340 | return tEnd; 341 | } 342 | 343 | /// 344 | /// Steps along a Bezier BACKWARDS from the given t factor, calculating the target t factor for the given spacing distance. 345 | /// Code based on Alterran's PropLineTool (StepDistanceCurve, Utilities/PLTMath.cs). 346 | /// 347 | /// Starting t factor. 348 | /// Distance to travel. 349 | /// Target t factor. 350 | private float BezierStepReverse(float tStart, float distance) 351 | { 352 | const float Tolerance = 0.001f; 353 | const float ToleranceSquared = Tolerance * Tolerance; 354 | 355 | float tEnd = Travel(tStart, -distance); 356 | float usedDistance = CubicBezierArcLengthXZGauss04(tEnd, tStart); 357 | 358 | // Twelve iteration maximum for performance and to prevent infinite loops. 359 | for (int i = 0; i < 12; ++i) 360 | { 361 | // Stop looping if the remaining distance is less than tolerance. 362 | float remainingDistance = distance - usedDistance; 363 | if (remainingDistance * remainingDistance < ToleranceSquared) 364 | { 365 | break; 366 | } 367 | 368 | usedDistance = CubicBezierArcLengthXZGauss04(tEnd, tStart); 369 | tEnd -= (distance - usedDistance) / CubicSpeedXZ(tEnd); 370 | } 371 | 372 | return tEnd; 373 | } 374 | 375 | /// 376 | /// From Alterann's PropLineTool (CubicSpeedXZ, Utilities/PLTMath.cs). 377 | /// Returns the integrand of the arc length function for a cubic Bezier curve, constrained to the XZ-plane at a specific t. 378 | /// 379 | /// t factor. 380 | /// Integrand of arc length. 381 | private float CubicSpeedXZ(float t) 382 | { 383 | // Pythagorean theorem. 384 | float3 tangent = MathUtils.Tangent(_thisBezier, t); 385 | float derivXsqr = tangent.x * tangent.x; 386 | float derivZsqr = tangent.z * tangent.z; 387 | 388 | return math.sqrt(derivXsqr + derivZsqr); 389 | } 390 | 391 | /// 392 | /// From Alterann's PropLineTool (CubicBezierArcLengthXZGauss04, Utilities/PLTMath.cs). 393 | /// Returns the XZ arclength of a cubic Bezier curve between two t factors. 394 | /// Uses Gauss–Legendre Quadrature with n = 4. 395 | /// 396 | /// Starting t factor. 397 | /// Ending t factor. 398 | /// XZ arc length. 399 | private float CubicBezierArcLengthXZGauss04(float t1, float t2) 400 | { 401 | float linearAdj = (t2 - t1) / 2f; 402 | 403 | // Constants are from Gauss-Lengendre quadrature rules for n = 4. 404 | float p1 = CubicSpeedXZGaussPoint(0.3399810435848563f, 0.6521451548625461f, t1, t2); 405 | float p2 = CubicSpeedXZGaussPoint(-0.3399810435848563f, 0.6521451548625461f, t1, t2); 406 | float p3 = CubicSpeedXZGaussPoint(0.8611363115940526f, 0.3478548451374538f, t1, t2); 407 | float p4 = CubicSpeedXZGaussPoint(-0.8611363115940526f, 0.3478548451374538f, t1, t2); 408 | 409 | return linearAdj * (p1 + p2 + p3 + p4); 410 | } 411 | 412 | /// 413 | /// From Alterann's PropLineTool (CubicSpeedXZGaussPoint, Utilities/PLTMath.cs). 414 | /// 415 | /// X i. 416 | /// W i. 417 | /// a. 418 | /// b. 419 | /// Cubic speed. 420 | private float CubicSpeedXZGaussPoint(float x_i, float w_i, float a, float b) 421 | { 422 | float linearAdj = (b - a) / 2f; 423 | float constantAdj = (a + b) / 2f; 424 | return w_i * CubicSpeedXZ((linearAdj * x_i) + constantAdj); 425 | } 426 | 427 | /// 428 | /// Based on CS1's mathematics calculations for Bezier travel. 429 | /// 430 | /// Starting t-factor. 431 | /// Distance to travel. 432 | /// Ending t-factor. 433 | private float Travel(float start, float distance) 434 | { 435 | Vector3 startPos = MathUtils.Position(_thisBezier, start); 436 | 437 | if (distance < 0f) 438 | { 439 | // Negative (reverse) direction. 440 | distance = 0f - distance; 441 | float startT = 0f; 442 | float endT = start; 443 | float startDistance = Vector3.SqrMagnitude(_thisBezier.a - (float3)startPos); 444 | float endDistance = 0f; 445 | 446 | // Eight steps max. 447 | for (int i = 0; i < 8; ++i) 448 | { 449 | // Calculate current position. 450 | float midT = (startT + endT) * 0.5f; 451 | Vector3 midpoint = MathUtils.Position(_thisBezier, midT); 452 | float midDistance = Vector3.SqrMagnitude(midpoint - startPos); 453 | 454 | // Check for nearer match. 455 | if (midDistance < distance * distance) 456 | { 457 | endT = midT; 458 | endDistance = midDistance; 459 | } 460 | else 461 | { 462 | startT = midT; 463 | startDistance = midDistance; 464 | } 465 | } 466 | 467 | // We've been using square magnitudes for comparison, so rest to true value. 468 | startDistance = Mathf.Sqrt(startDistance); 469 | endDistance = Mathf.Sqrt(endDistance); 470 | 471 | // Check for exact match. 472 | float fDiff = startDistance - endDistance; 473 | if (fDiff == 0f) 474 | { 475 | // Exact match found - return that. 476 | return endT; 477 | } 478 | 479 | // Not an exact match - use an interpolation. 480 | return Mathf.Lerp(endT, startT, Mathf.Clamp01((distance - endDistance) / fDiff)); 481 | } 482 | else 483 | { 484 | // Positive (forward) direction. 485 | float startT = start; 486 | float endT = 1f; 487 | float startDistance = 0f; 488 | float endDistance = Vector3.SqrMagnitude(_thisBezier.d - (float3)startPos); 489 | 490 | // Eight steps max. 491 | for (int i = 0; i < 8; ++i) 492 | { 493 | // Calculate current position. 494 | float tMid = (startT + endT) * 0.5f; 495 | Vector3 midPoint = MathUtils.Position(_thisBezier, tMid); 496 | float midDistance = Vector3.SqrMagnitude(midPoint - startPos); 497 | 498 | // Check for nearer match. 499 | if (midDistance < distance * distance) 500 | { 501 | startT = tMid; 502 | startDistance = midDistance; 503 | } 504 | else 505 | { 506 | endT = tMid; 507 | endDistance = midDistance; 508 | } 509 | } 510 | 511 | // We've been using square magnitudes for comparison, so rest to true value. 512 | startDistance = Mathf.Sqrt(startDistance); 513 | endDistance = Mathf.Sqrt(endDistance); 514 | 515 | // Check for exact match. 516 | float remainder = endDistance - startDistance; 517 | if (remainder == 0f) 518 | { 519 | // Exact match found - return that. 520 | return startT; 521 | } 522 | 523 | // Not an exact match - use an interpolation. 524 | return Mathf.Lerp(startT, endT, Mathf.Clamp01((distance - startDistance) / remainder)); 525 | } 526 | } 527 | 528 | /// 529 | /// Draws an angle indicator between two lines. 530 | /// 531 | /// Line 1. 532 | /// Line 2. 533 | /// Overlay line width. 534 | /// Overlay line length. 535 | /// Overlay buffer. 536 | /// Tooltip list. 537 | private void DrawAngleIndicator(Line3.Segment line1, Line3.Segment line2, float lineWidth, float lineLength, OverlayRenderSystem.Buffer overlayBuffer, NativeList tooltips) 538 | { 539 | bool angleSide = false; 540 | 541 | // Calculate line lengths. 542 | float line1Length = math.distance(line1.a.xz, line1.b.xz); 543 | float line2Length = math.distance(line2.a.xz, line2.b.xz); 544 | 545 | // Minimum line length check. 546 | if (line1Length > lineWidth * 7f && line2Length > lineWidth * 7f) 547 | { 548 | // Calculate line directions. 549 | float2 line1Direction = (line1.b.xz - line1.a.xz) / line1Length; 550 | float2 line2Direction = (line2.a.xz - line2.b.xz) / line2Length; 551 | 552 | // Display size. 553 | float size = math.min(lineLength, math.min(line1Length, line2Length)) * 0.5f; 554 | 555 | // Calculate angle and determine shortest side. 556 | int angle = Mathf.RoundToInt(math.degrees(math.acos(math.clamp(math.dot(line1Direction, line2Direction), -1f, 1f)))); 557 | if (angle < 180) 558 | { 559 | angleSide = math.dot(MathUtils.Right(line1Direction), line2Direction) < 0f; 560 | } 561 | 562 | // Check angle type - straight line, obtuse, right-angle, acute. 563 | if (angle == 180) 564 | { 565 | // Straight line - three lines. 566 | // Get perpendiculars. 567 | float2 angle1Direction = angleSide ? MathUtils.Right(line1Direction) : MathUtils.Left(line1Direction); 568 | float2 angle2Direction = angleSide ? MathUtils.Right(line2Direction) : MathUtils.Left(line2Direction); 569 | float3 line1Start = line1.b; 570 | line1Start.xz -= line1Direction * size; 571 | 572 | // Calculate three lines. 573 | float3 line1End = line1.b; 574 | float3 line2Start = line1.b; 575 | line1End.xz += (angle1Direction * (size - (lineWidth * 0.5f))) - (line1Direction * size); 576 | line2Start.xz += (angle1Direction * size) - (line1Direction * (size + (lineWidth * 0.5f))); 577 | float3 line2End = line2.a; 578 | float3 line3Start = line2.a; 579 | line2End.xz -= (angle2Direction * size) + (line2Direction * (size + (lineWidth * 0.5f))); 580 | line3Start.xz -= (angle2Direction * (size - (lineWidth * 0.5f))) + (line2Direction * size); 581 | float3 line3End = line2.a; 582 | line3End.xz -= line2Direction * size; 583 | 584 | // Draw lines. 585 | overlayBuffer.DrawLine(Color.white, new Line3.Segment(line1Start, line1End), lineWidth); 586 | overlayBuffer.DrawLine(Color.white, new Line3.Segment(line2Start, line2End), lineWidth); 587 | overlayBuffer.DrawLine(Color.white, new Line3.Segment(line3Start, line3End), lineWidth); 588 | 589 | // Add tooltip. 590 | float3 tooltipPos = line1.b; 591 | tooltipPos.xz += angle1Direction * (size * 1.5f); 592 | TooltipInfo value = new (TooltipType.Angle, tooltipPos, angle); 593 | tooltips.Add(in value); 594 | } 595 | else if (angle > 90) 596 | { 597 | // Obtuse angle - two angle indicators. 598 | float2 angleDirection = math.normalize(line1Direction + line2Direction); 599 | float3 startPoint = line1.b; 600 | 601 | // Calculate two sequential curves. 602 | startPoint.xz -= line1Direction * size; 603 | float3 startTangent = default; 604 | startTangent.xz = angleSide ? MathUtils.Right(line1Direction) : MathUtils.Left(line1Direction); 605 | float3 midPoint = line1.b; 606 | midPoint.xz -= angleDirection * size; 607 | float3 midTangent = default; 608 | midTangent.xz = angleSide ? MathUtils.Right(angleDirection) : MathUtils.Left(angleDirection); 609 | float3 endPoint = line2.a; 610 | endPoint.xz -= line2Direction * size; 611 | float3 endTangent = default; 612 | endTangent.xz = angleSide ? MathUtils.Right(line2Direction) : MathUtils.Left(line2Direction); 613 | 614 | // Draw curves. 615 | overlayBuffer.DrawCurve(Color.white, NetUtils.FitCurve(startPoint, startTangent, midTangent, midPoint), lineWidth); 616 | overlayBuffer.DrawCurve(Color.white, NetUtils.FitCurve(midPoint, midTangent, endTangent, endPoint), lineWidth); 617 | 618 | // Add tooltip. 619 | float3 tooltipPos = line1.b; 620 | tooltipPos.xz -= angleDirection * (size * 1.5f); 621 | TooltipInfo value = new (TooltipType.Angle, tooltipPos, angle); 622 | tooltips.Add(in value); 623 | } 624 | else if (angle == 90) 625 | { 626 | // Right angle - two lines. 627 | float3 line1Start = line1.b; 628 | line1Start.xz -= line1Direction * size; 629 | 630 | // Calculate two lines. 631 | float3 line1End = line1.b; 632 | float3 line2Start = line1.b; 633 | line1End.xz -= (line2Direction * (size - (lineWidth * 0.5f))) + (line1Direction * size); 634 | line2Start.xz -= (line2Direction * size) + (line1Direction * (size + (lineWidth * 0.5f))); 635 | float3 line2End = line2.a; 636 | line2End.xz -= line2Direction * size; 637 | 638 | // Draw lines. 639 | overlayBuffer.DrawLine(Color.white, new Line3.Segment(line1Start, line1End), lineWidth); 640 | overlayBuffer.DrawLine(Color.white, new Line3.Segment(line2Start, line2End), lineWidth); 641 | 642 | // Add tooltip. 643 | float3 tooltipPos = line1.b; 644 | tooltipPos.xz -= math.normalizesafe(line1Direction + line2Direction) * (size * 1.5f); 645 | TooltipInfo value = new (TooltipType.Angle, tooltipPos, angle); 646 | tooltips.Add(in value); 647 | } 648 | else if (angle > 0) 649 | { 650 | // Acute angle - one angle indicator. 651 | float3 startPos = line1.b; 652 | startPos.xz -= line1Direction * size; 653 | 654 | // Calculate single curve. 655 | float3 startTangent = default; 656 | startTangent.xz = angleSide ? MathUtils.Right(line1Direction) : MathUtils.Left(line1Direction); 657 | float3 endPos = line2.a; 658 | endPos.xz -= line2Direction * size; 659 | float3 endTangent = default; 660 | endTangent.xz = angleSide ? MathUtils.Right(line2Direction) : MathUtils.Left(line2Direction); 661 | 662 | // Draw curve. 663 | overlayBuffer.DrawCurve(Color.white, NetUtils.FitCurve(startPos, startTangent, endTangent, endPos), lineWidth); 664 | 665 | // Add tooltip. 666 | float3 tooltipPos = line1.b; 667 | tooltipPos.xz -= math.normalizesafe(line1Direction + line2Direction) * (size * 1.5f); 668 | TooltipInfo value = new (TooltipType.Angle, tooltipPos, angle); 669 | tooltips.Add(in value); 670 | } 671 | } 672 | } 673 | } 674 | } 675 | -------------------------------------------------------------------------------- /Code/LineModes/SpacingMode.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | namespace LineTool 8 | { 9 | /// 10 | /// Line tool modes. 11 | /// 12 | public enum SpacingMode 13 | { 14 | /// 15 | /// Manually spaced. 16 | /// 17 | Manual, 18 | 19 | /// 20 | /// Fence mode. 21 | /// 22 | FenceMode, 23 | 24 | /// 25 | /// Evenly spaced along entire length of line. 26 | /// 27 | FullLength, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Code/LineModes/StraightLine.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | namespace LineTool 8 | { 9 | /// 10 | /// Straight-line placement mode. 11 | /// 12 | public class StraightLine : LineBase 13 | { 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | public StraightLine() 18 | { 19 | // Basic state. 20 | m_validStart = false; 21 | } 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// Mode to copy starting state from. 27 | public StraightLine(LineBase mode) 28 | : base(mode) 29 | { 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Code/Localization.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | namespace LineTool 8 | { 9 | using System; 10 | using System.Collections.Generic; 11 | using System.IO; 12 | using System.Linq; 13 | using System.Reflection; 14 | using Colossal.Localization; 15 | using Colossal.Logging; 16 | using Game.SceneFlow; 17 | 18 | /// 19 | /// Translation handling. 20 | /// 21 | public static class Localization 22 | { 23 | /// 24 | /// Loads settings translations from tab-separated l10n file. 25 | /// 26 | /// Log to use. 27 | public static void LoadTranslations(ILog log) 28 | { 29 | try 30 | { 31 | // Read embedded file. 32 | using StreamReader reader = new (Assembly.GetExecutingAssembly().GetManifestResourceStream("LineToolLite.l10n.csv")); 33 | { 34 | List lines = new (); 35 | while (!reader.EndOfStream) 36 | { 37 | // Skip empty lines. 38 | string line = reader.ReadLine(); 39 | if (!string.IsNullOrWhiteSpace(line)) 40 | { 41 | lines.Add(line); 42 | } 43 | } 44 | 45 | // Iterate through each game locale. 46 | log.Info("parsing translation file"); 47 | IEnumerable fileLines = lines.Select(x => x.Split('\t')); 48 | foreach (string localeID in GameManager.instance.localizationManager.GetSupportedLocales()) 49 | { 50 | try 51 | { 52 | // Find matching column in file. 53 | int valueColumn = Array.IndexOf(fileLines.First(), localeID); 54 | 55 | // Make sure a valid column has been found (column 0 is the binding context and column 1 is the translation key). 56 | if (valueColumn > 1) 57 | { 58 | // Add translations to game locales. 59 | log.Debug("found translation for " + localeID); 60 | MemorySource language = new (fileLines.Skip(1).ToDictionary(x => x[0] + '.' + x[1], x => x.ElementAtOrDefault(valueColumn))); 61 | GameManager.instance.localizationManager.AddSource(localeID, language); 62 | } 63 | } 64 | catch (Exception e) 65 | { 66 | // Don't let a single failure stop us. 67 | log.Error(e, $"exception reading localization for locale {localeID}"); 68 | } 69 | } 70 | } 71 | } 72 | catch (Exception e) 73 | { 74 | log.Error(e, "exception reading settings localization file"); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Code/Mod.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | namespace LineTool 8 | { 9 | using System.IO; 10 | using System.Reflection; 11 | using Colossal.IO.AssetDatabase; 12 | using Colossal.Logging; 13 | using Game; 14 | using Game.Modding; 15 | using Game.SceneFlow; 16 | using Game.UI; 17 | 18 | /// 19 | /// The base mod class for instantiation by the game. 20 | /// 21 | public sealed class Mod : IMod 22 | { 23 | /// 24 | /// The mod's default name. 25 | /// 26 | public const string ModName = "Line Tool"; 27 | 28 | /// 29 | /// Gets the active instance reference. 30 | /// 31 | public static Mod Instance { get; private set; } 32 | 33 | /// 34 | /// Gets the mod's active log. 35 | /// 36 | internal ILog Log { get; private set; } 37 | 38 | /// 39 | /// Called by the game when the mod is loaded. 40 | /// 41 | public void OnLoad() 42 | { 43 | // Set instance reference. 44 | Instance = this; 45 | 46 | // Initialize logger. 47 | Log = LogManager.GetLogger(ModName); 48 | #if DEBUG 49 | Log.Info("setting logging level to Debug"); 50 | Log.effectivenessLevel = Level.Debug; 51 | #endif 52 | 53 | Log.Info($"loading {ModName} version {Assembly.GetExecutingAssembly().GetName().Version}"); 54 | } 55 | 56 | /// 57 | /// Called by the game when the game world is created. 58 | /// 59 | /// Game update system. 60 | public void OnCreateWorld(UpdateSystem updateSystem) 61 | { 62 | Log.Info("starting OnCreateWorld"); 63 | 64 | // Load translations. 65 | Localization.LoadTranslations(Log); 66 | 67 | // Activate systems. 68 | updateSystem.UpdateAt(SystemUpdatePhase.ToolUpdate); 69 | updateSystem.UpdateAt(SystemUpdatePhase.UIUpdate); 70 | updateSystem.UpdateAt(SystemUpdatePhase.UITooltip); 71 | 72 | // Add mod UI icons to UI resource handler. 73 | GameUIResourceHandler uiResourceHandler = GameManager.instance.userInterface.view.uiSystem.resourceHandler as GameUIResourceHandler; 74 | uiResourceHandler?.HostLocationsMap.Add("linetool", new System.Collections.Generic.List { Path.GetDirectoryName(typeof(Plugin).Assembly.Location) + "/" }); 75 | } 76 | 77 | /// 78 | /// Called by the game when the mod is disposed of. 79 | /// 80 | public void OnDispose() 81 | { 82 | Log.Info("disposing"); 83 | Instance = null; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Code/Systems/LineToolSystem.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | namespace LineTool 8 | { 9 | using System; 10 | using System.Reflection; 11 | using Colossal.Entities; 12 | using Colossal.Logging; 13 | using Colossal.Mathematics; 14 | using Colossal.Serialization.Entities; 15 | using Game; 16 | using Game.Common; 17 | using Game.Input; 18 | using Game.Objects; 19 | using Game.Prefabs; 20 | using Game.Rendering; 21 | using Game.Simulation; 22 | using Game.Tools; 23 | using Unity.Collections; 24 | using Unity.Entities; 25 | using Unity.Jobs; 26 | using Unity.Mathematics; 27 | using UnityEngine.InputSystem; 28 | using static Game.Rendering.GuideLinesSystem; 29 | using Random = Unity.Mathematics.Random; 30 | using Transform = Game.Objects.Transform; 31 | using Tree = Game.Objects.Tree; 32 | 33 | /// 34 | /// Line tool system. 35 | /// 36 | public sealed partial class LineToolSystem : ObjectToolBaseSystem 37 | { 38 | // Previewing. 39 | private readonly NativeList _previewEntities = new (Allocator.Persistent); 40 | private readonly NativeList _tooltips = new (8, Allocator.Persistent); 41 | 42 | // Line calculations. 43 | private readonly NativeList _points = new (Allocator.Persistent); 44 | private bool _fixedPreview = false; 45 | private float3 _fixedPos; 46 | private Random _random = new (); 47 | 48 | // Cursor. 49 | private ControlPoint _raycastPoint; 50 | private float3 _previousPos; 51 | private Entity _cursorEntity = Entity.Null; 52 | 53 | // Prefab selection. 54 | private ToolBaseSystem _previousTool = null; 55 | private ObjectGeometryPrefab _selectedPrefab; 56 | private Entity _selectedEntity = Entity.Null; 57 | private int _originalXP; 58 | private Bounds1 _zBounds; 59 | 60 | // References. 61 | private ILog _log; 62 | private TerrainSystem _terrainSystem; 63 | private TerrainHeightData _terrainHeightData; 64 | private OverlayRenderSystem.Buffer _overlayBuffer; 65 | 66 | // Input actions. 67 | private ProxyAction _applyAction; 68 | private ProxyAction _cancelAction; 69 | private InputAction _fixedPreviewAction; 70 | private InputAction _keepBuildingAction; 71 | 72 | // Mode. 73 | private LineBase _mode; 74 | private LineMode _currentMode; 75 | private DragMode _dragMode = DragMode.None; 76 | 77 | // Tool settings. 78 | private SpacingMode _spacingMode = SpacingMode.Manual; 79 | private float _spacing = 20f; 80 | private bool _randomRotation = false; 81 | private int _rotation = 0; 82 | private float _randomSpacing = 0f; 83 | private float _randomOffset = 0f; 84 | private bool _dirty = false; 85 | 86 | // Tree Controller integration. 87 | private ToolBaseSystem _treeControllerTool; 88 | private PropertyInfo _nextTreeState = null; 89 | 90 | /// 91 | /// Point dragging mode. 92 | /// 93 | internal enum DragMode 94 | { 95 | /// 96 | /// No dragging. 97 | /// 98 | None = 0, 99 | 100 | /// 101 | /// Dragging the line's start position. 102 | /// 103 | StartPos, 104 | 105 | /// 106 | /// Dragging the line's end position. 107 | /// 108 | EndPos, 109 | 110 | /// 111 | /// Dragging the line's elbow position. 112 | /// 113 | ElbowPos, 114 | } 115 | 116 | /// 117 | /// Gets the tool's ID string. 118 | /// 119 | public override string toolID => "Line Tool"; 120 | 121 | /// 122 | /// Gets or sets the effective line spacing. 123 | /// 124 | internal float Spacing 125 | { 126 | get => _spacing; 127 | 128 | set 129 | { 130 | // Don't allow spacing to be set smaller than the smallest side of zBounds. 131 | _spacing = (float)Math.Round(math.max(value, math.max(math.abs(_zBounds.max), math.abs(_zBounds.min) + 0.1f)), 1); 132 | World.GetOrCreateSystemManaged().UpdateSpacing(); 133 | _dirty = true; 134 | } 135 | } 136 | 137 | /// 138 | /// Gets the effective spacing value, taking into account fence mode. 139 | /// 140 | internal float EffectiveSpacing => _spacingMode == SpacingMode.FenceMode ? _zBounds.max - _zBounds.min : _spacing; 141 | 142 | /// 143 | /// Gets or sets the current spacing mode. 144 | /// 145 | internal SpacingMode CurrentSpacingMode 146 | { 147 | get => _spacingMode; 148 | set 149 | { 150 | _spacingMode = value; 151 | _dirty = true; 152 | } 153 | } 154 | 155 | /// 156 | /// Gets or sets a value indicating whether random rotation is active. 157 | /// 158 | internal bool RandomRotation 159 | { 160 | get => _randomRotation; 161 | set 162 | { 163 | _randomRotation = value; 164 | _dirty = true; 165 | } 166 | } 167 | 168 | /// 169 | /// Gets or sets the random spacing offset maximum. 170 | /// 171 | internal float RandomSpacing 172 | { 173 | get => _randomSpacing; 174 | set 175 | { 176 | _randomSpacing = value; 177 | _dirty = true; 178 | } 179 | } 180 | 181 | /// 182 | /// Gets or sets the random lateral offset maximum. 183 | /// 184 | internal float RandomOffset 185 | { 186 | get => _randomOffset; 187 | set 188 | { 189 | _randomOffset = value; 190 | _dirty = true; 191 | } 192 | } 193 | 194 | /// 195 | /// Gets or sets the rotation setting. 196 | /// 197 | internal int Rotation 198 | { 199 | get => _rotation; 200 | set 201 | { 202 | _rotation = value; 203 | _dirty = true; 204 | } 205 | } 206 | 207 | /// 208 | /// Gets the tooltip list. 209 | /// 210 | internal NativeList Tooltips => _tooltips; 211 | 212 | /// 213 | /// Gets or sets the current line mode. 214 | /// 215 | internal LineMode Mode 216 | { 217 | get => _currentMode; 218 | 219 | set 220 | { 221 | // Don't do anything if no change. 222 | if (value == _currentMode) 223 | { 224 | return; 225 | } 226 | 227 | // Apply updated tool mode. 228 | switch (value) 229 | { 230 | case LineMode.Straight: 231 | _mode = new StraightLine(_mode); 232 | break; 233 | case LineMode.SimpleCurve: 234 | _mode = new SimpleCurve(_mode); 235 | break; 236 | case LineMode.Circle: 237 | _mode = new Circle(_mode); 238 | break; 239 | } 240 | 241 | // Update mode. 242 | _currentMode = value; 243 | } 244 | } 245 | 246 | /// 247 | /// Gets the currently selected entity. 248 | /// 249 | internal Entity SelectedEntity => _selectedEntity; 250 | 251 | /// 252 | /// Sets the currently selected prefab. 253 | /// 254 | private PrefabBase SelectedPrefab 255 | { 256 | set 257 | { 258 | _selectedPrefab = value as ObjectGeometryPrefab; 259 | 260 | // Update selected entity. 261 | if (_selectedPrefab is null) 262 | { 263 | // No valid entity selected. 264 | _selectedEntity = Entity.Null; 265 | } 266 | else 267 | { 268 | // Get selected entity. 269 | _selectedEntity = m_PrefabSystem.GetEntity(_selectedPrefab); 270 | 271 | // Check bounds. 272 | _zBounds.min = 0; 273 | _zBounds.max = 0; 274 | foreach (ObjectMeshInfo mesh in _selectedPrefab.m_Meshes) 275 | { 276 | if (mesh.m_Mesh is RenderPrefab renderPrefab) 277 | { 278 | // Update bounds if either of the z extents of this mesh exceed the previous extent. 279 | _zBounds.min = math.min(_zBounds.min, renderPrefab.bounds.z.min); 280 | _zBounds.max = math.max(_zBounds.max, renderPrefab.bounds.z.max); 281 | } 282 | } 283 | 284 | // Reduce any XP to zero while we're using the tool. 285 | SaveXP(); 286 | } 287 | } 288 | } 289 | 290 | /// 291 | /// Called when the raycast is initialized. 292 | /// 293 | public override void InitializeRaycast() 294 | { 295 | base.InitializeRaycast(); 296 | 297 | // Set raycast mask. 298 | m_ToolRaycastSystem.typeMask = TypeMask.Terrain; 299 | } 300 | 301 | /// 302 | /// Gets the prefab selected by this tool. 303 | /// 304 | /// Cnull. 305 | public override PrefabBase GetPrefab() => _selectedPrefab; 306 | 307 | /// 308 | /// Sets the prefab selected by this tool. 309 | /// 310 | /// Prefab to set. 311 | /// true uf the previously-used tool (if any) can use this prefab, otherwise false. 312 | public override bool TrySetPrefab(PrefabBase prefab) => _previousTool?.TrySetPrefab(prefab) ?? false; 313 | 314 | /// 315 | /// Elevation-up key handler; used to increment spacing. 316 | /// 317 | public override void ElevationUp() => Spacing = _spacing + 1; 318 | 319 | /// 320 | /// Elevation-down key handler; used to decrement spacing. 321 | /// 322 | public override void ElevationDown() => Spacing = _spacing - 1; 323 | 324 | /// 325 | /// Enables the tool (called by hotkey action). 326 | /// 327 | internal void EnableTool() 328 | { 329 | // Activate this tool if it isn't already active. 330 | if (m_ToolSystem.activeTool != this) 331 | { 332 | _previousTool = m_ToolSystem.activeTool; 333 | 334 | // Check for valid prefab selection before continuing. 335 | SelectedPrefab = World.GetOrCreateSystemManaged().prefab; 336 | if (_selectedPrefab != null) 337 | { 338 | // Valid prefab selected - switch to this tool. 339 | m_ToolSystem.selected = Entity.Null; 340 | m_ToolSystem.activeTool = this; 341 | } 342 | } 343 | } 344 | 345 | /// 346 | /// Restores the previously-used tool. 347 | /// 348 | internal void RestorePreviousTool() 349 | { 350 | if (_previousTool is not null) 351 | { 352 | m_ToolSystem.activeTool = _previousTool; 353 | } 354 | else 355 | { 356 | _log.Error("null tool set when restoring previous tool"); 357 | } 358 | } 359 | 360 | /// 361 | /// Refreshes all displayed prefabs to align with current Tree Control settings. 362 | /// 363 | internal void RefreshTreeControl() 364 | { 365 | // Update cursor entity. 366 | ResetTreeState(_cursorEntity); 367 | 368 | // Update all previewed trees. 369 | for (int i = 0; i < _previewEntities.Length; ++i) 370 | { 371 | ResetTreeState(_previewEntities[i]); 372 | } 373 | 374 | // Set dirty flag. 375 | _dirty = true; 376 | } 377 | 378 | /// 379 | /// Called when the system is created. 380 | /// 381 | protected override void OnCreate() 382 | { 383 | base.OnCreate(); 384 | 385 | // Set log. 386 | _log = Mod.Instance.Log; 387 | 388 | // Get system references. 389 | _terrainSystem = World.GetOrCreateSystemManaged(); 390 | _overlayBuffer = World.GetOrCreateSystemManaged().GetBuffer(out var _); 391 | 392 | // Set default mode. 393 | _currentMode = LineMode.Straight; 394 | _mode = new StraightLine(); 395 | 396 | // Set actions. 397 | _applyAction = InputManager.instance.FindAction("Tool", "Apply"); 398 | _cancelAction = InputManager.instance.FindAction("Tool", "Mouse Cancel"); 399 | 400 | // Enable fixed preview control. 401 | _fixedPreviewAction = new ("LineTool-FixPreview"); 402 | _fixedPreviewAction.AddCompositeBinding("ButtonWithOneModifier").With("Modifier", "/ctrl").With("Button", "/leftButton"); 403 | _fixedPreviewAction.Enable(); 404 | 405 | // Enable keep building action. 406 | _keepBuildingAction = new ("LineTool-KeepBuilding"); 407 | _keepBuildingAction.AddCompositeBinding("ButtonWithOneModifier").With("Modifier", "/shift").With("Button", "/leftButton"); 408 | _keepBuildingAction.Enable(); 409 | } 410 | 411 | /// 412 | /// Called by the game when loading is complete. 413 | /// 414 | /// Loading purpose. 415 | /// Current game mode. 416 | protected override void OnGameLoadingComplete(Purpose purpose, GameMode mode) 417 | { 418 | base.OnGameLoadingComplete(purpose, mode); 419 | 420 | // Try to get tree controller tool. 421 | if (World.GetOrCreateSystemManaged().tools.Find(x => x.toolID.Equals("Tree Controller Tool")) is ToolBaseSystem treeControllerTool) 422 | { 423 | // Found it - attempt to reflect NextTreeState property getter. 424 | _log.Info("found tree controller"); 425 | _nextTreeState = treeControllerTool.GetType().GetProperty("NextTreeState"); 426 | if (_nextTreeState is not null) 427 | { 428 | _treeControllerTool = treeControllerTool; 429 | _log.Info("reflected NextTreeState"); 430 | } 431 | } 432 | else 433 | { 434 | _log.Info("tree controller tool not found"); 435 | } 436 | } 437 | 438 | /// 439 | /// Called every tool update. 440 | /// 441 | /// Input dependencies. 442 | /// Job handle. 443 | protected override JobHandle OnUpdate(JobHandle inputDeps) 444 | { 445 | // Clear tooltips. 446 | _tooltips.Clear(); 447 | 448 | // Don't do anything if no selected prefab. 449 | if (_selectedPrefab is null) 450 | { 451 | return inputDeps; 452 | } 453 | 454 | // Check for valid raycast. 455 | float3 position = _fixedPreview ? _fixedPos : _previousPos; 456 | if (GetRaycastResult(out _raycastPoint)) 457 | { 458 | // Valid raycast - update position. 459 | position = _fixedPreview ? _fixedPos : _raycastPoint.m_HitPosition; 460 | 461 | // Calculate terrain height. 462 | _terrainHeightData = _terrainSystem.GetHeightData(); 463 | position.y = TerrainUtils.SampleHeight(ref _terrainHeightData, position); 464 | 465 | // Handle any dragging. 466 | if (_dragMode != DragMode.None) 467 | { 468 | if (_applyAction.WasReleasedThisFrame() || _fixedPreviewAction.WasReleasedThisFrame()) 469 | { 470 | // Cancel dragging. 471 | _dragMode = DragMode.None; 472 | } 473 | else 474 | { 475 | // Drag end point. 476 | if (_dragMode == DragMode.EndPos) 477 | { 478 | position = _raycastPoint.m_HitPosition; 479 | _fixedPos = position; 480 | } 481 | else 482 | { 483 | // Handle dragging for other points via line mode instance. 484 | _mode.HandleDrag(_dragMode, _raycastPoint.m_HitPosition); 485 | } 486 | 487 | _dirty = true; 488 | } 489 | } 490 | 491 | // Check for and perform any cancellation. 492 | if (_cancelAction.WasPressedThisFrame()) 493 | { 494 | // Reset current mode settings. 495 | _mode.Reset(); 496 | 497 | // Revert previewing. 498 | foreach (Entity previewEntity in _previewEntities) 499 | { 500 | EntityManager.AddComponent(previewEntity); 501 | } 502 | 503 | _previewEntities.Clear(); 504 | _dragMode = DragMode.None; 505 | 506 | return inputDeps; 507 | } 508 | 509 | // If no cancellation, handle any fixed preview action if we're ready to place. 510 | else if (_fixedPreviewAction.WasPressedThisFrame() && _mode.HasAllPoints) 511 | { 512 | // Are we already in fixed preview mode? 513 | if (_fixedPreview) 514 | { 515 | // Already in fixed preview mode - check for dragging hits. 516 | _dragMode = _mode.CheckDragHit(_raycastPoint.m_HitPosition); 517 | if (_dragMode != DragMode.None) 518 | { 519 | // If dragging, has started, then we're done here. 520 | return inputDeps; 521 | } 522 | } 523 | else 524 | { 525 | // Activate fixed preview mode and fix current position. 526 | _fixedPreview = true; 527 | _fixedPos = position; 528 | } 529 | } 530 | 531 | // Handle apply action if no other actions. 532 | else if (_applyAction.WasPressedThisFrame() || _keepBuildingAction.WasPressedThisFrame()) 533 | { 534 | // Were we in fixed state? 535 | if (_fixedPreview) 536 | { 537 | // Check for dragging hits. 538 | _dragMode = _mode.CheckDragHit(_raycastPoint.m_HitPosition); 539 | if (_dragMode != DragMode.None) 540 | { 541 | // If dragging, has started, then we're done here. 542 | return inputDeps; 543 | } 544 | 545 | // Yes - cancel fixed preview. 546 | _fixedPreview = false; 547 | } 548 | 549 | // Handle click. 550 | if (_mode.HandleClick(position)) 551 | { 552 | // We're placing items - remove highlighting. 553 | foreach (Entity previewEntity in _previewEntities) 554 | { 555 | if (EntityManager.HasComponent(previewEntity)) 556 | { 557 | EntityManager.AddComponent(previewEntity); 558 | } 559 | else 560 | { 561 | EntityManager.RemoveComponent(previewEntity); 562 | EntityManager.AddComponent(previewEntity); 563 | } 564 | } 565 | 566 | // Clear preview. 567 | _previewEntities.Clear(); 568 | 569 | // Perform post-placement. 570 | _mode.ItemsPlaced(position); 571 | 572 | // Reset tool mode if we're not building continuously. 573 | if (!_keepBuildingAction.WasPressedThisFrame()) 574 | { 575 | _mode.Reset(); 576 | } 577 | 578 | return inputDeps; 579 | } 580 | } 581 | 582 | // Update cursor entity if we haven't got an initial position set. 583 | if (!_mode.HasStart) 584 | { 585 | // Don't update if the cursor hasn't moved. 586 | if (position.x != _previousPos.x || position.z != _previousPos.z) 587 | { 588 | // Delete any existing cursor entity and create a new one. 589 | if (_cursorEntity != Entity.Null) 590 | { 591 | EntityManager.AddComponent(_cursorEntity); 592 | } 593 | 594 | _cursorEntity = CreateEntity(); 595 | 596 | // Highlight cursor entity. 597 | EntityManager.AddComponent(_cursorEntity); 598 | 599 | // Update cursor entity position. 600 | EntityManager.SetComponentData(_cursorEntity, new Transform { m_Position = position, m_Rotation = GetEffectiveRotation(position) }); 601 | EntityManager.AddComponent(_cursorEntity); 602 | 603 | // Ensure cursor entity tree state. 604 | EnsureTreeState(_cursorEntity); 605 | 606 | // Update previous position. 607 | _previousPos = position; 608 | } 609 | 610 | return inputDeps; 611 | } 612 | else if (_cursorEntity != Entity.Null) 613 | { 614 | // Cancel cursor entity. 615 | EntityManager.AddComponent(_cursorEntity); 616 | _cursorEntity = Entity.Null; 617 | } 618 | } 619 | else 620 | { 621 | // No valid raycast - hide cursor. 622 | if (_cursorEntity != Entity.Null) 623 | { 624 | EntityManager.AddComponent(_cursorEntity); 625 | } 626 | } 627 | 628 | // Render any overlay. 629 | _mode.DrawOverlay(_overlayBuffer, _tooltips); 630 | 631 | // Overlay control points. 632 | if (_fixedPreview) 633 | { 634 | _mode.DrawPointOverlays(_overlayBuffer); 635 | } 636 | 637 | // Check for position change or update needed. 638 | if (!_dirty && position.x == _previousPos.x && position.z == _previousPos.y) 639 | { 640 | // No update needed. 641 | return inputDeps; 642 | } 643 | 644 | // Update stored position and clear dirty flag. 645 | _previousPos = position; 646 | _dirty = false; 647 | 648 | // If we got here we're (re)calculating points. 649 | _points.Clear(); 650 | _mode.CalculatePoints(position, _spacingMode, EffectiveSpacing, RandomSpacing, RandomOffset, _rotation, _zBounds, _points, ref _terrainHeightData); 651 | 652 | // Clear all preview entities. 653 | foreach (Entity entity in _previewEntities) 654 | { 655 | EntityManager.AddComponent(entity); 656 | } 657 | 658 | _previewEntities.Clear(); 659 | 660 | // Step along length and place preview objects. 661 | foreach (PointData thisPoint in _points) 662 | { 663 | UnityEngine.Random.InitState((int)(thisPoint.Position.x + thisPoint.Position.y + thisPoint.Position.z)); 664 | 665 | // Create transform component. 666 | Transform transformData = new () 667 | { 668 | m_Position = thisPoint.Position, 669 | m_Rotation = _randomRotation ? GetEffectiveRotation(thisPoint.Position) : thisPoint.Rotation, 670 | }; 671 | 672 | // Create new entity. 673 | Entity newEntity = CreateEntity(); 674 | EntityManager.SetComponentData(newEntity, transformData); 675 | EntityManager.AddComponent(newEntity); 676 | EntityManager.AddComponent(newEntity); 677 | _previewEntities.Add(newEntity); 678 | } 679 | 680 | return inputDeps; 681 | } 682 | 683 | /// 684 | /// Called when the tool starts running. 685 | /// 686 | protected override void OnStartRunning() 687 | { 688 | _log.Debug("OnStartRunning"); 689 | base.OnStartRunning(); 690 | 691 | // Clear any existing tooltips. 692 | World.GetExistingSystemManaged().ClearTooltip(); 693 | 694 | // Ensure apply action is enabled. 695 | _applyAction.shouldBeEnabled = true; 696 | _cancelAction.shouldBeEnabled = true; 697 | 698 | // Clear any previous raycast result. 699 | _raycastPoint = default; 700 | 701 | // Reset any previously-stored starting position. 702 | _mode.Reset(); 703 | 704 | // Clear any applications. 705 | applyMode = ApplyMode.Clear; 706 | } 707 | 708 | /// 709 | /// Called when the tool stops running. 710 | /// 711 | protected override void OnStopRunning() 712 | { 713 | _log.Debug("OnStopRunning"); 714 | 715 | // Clear tooltips. 716 | _tooltips.Clear(); 717 | World.GetExistingSystemManaged().ClearTooltip(); 718 | 719 | // Disable apply action. 720 | _applyAction.shouldBeEnabled = false; 721 | _cancelAction.shouldBeEnabled = false; 722 | 723 | // Cancel cursor entity. 724 | if (_cursorEntity != Entity.Null) 725 | { 726 | EntityManager.AddComponent(_cursorEntity); 727 | _cursorEntity = Entity.Null; 728 | } 729 | 730 | // Revert previewing. 731 | foreach (Entity previewEntity in _previewEntities) 732 | { 733 | EntityManager.AddComponent(previewEntity); 734 | } 735 | 736 | // Clear previewed entity buffer. 737 | _previewEntities.Clear(); 738 | 739 | // Restore prefab XP. 740 | RestoreXP(); 741 | 742 | // Reset state. 743 | _mode.Reset(); 744 | 745 | base.OnStopRunning(); 746 | } 747 | 748 | /// 749 | /// Called when the system is destroyed. 750 | /// 751 | protected override void OnDestroy() 752 | { 753 | // Dispose of unmanaged lists. 754 | _previewEntities.Dispose(); 755 | _points.Dispose(); 756 | _tooltips.Dispose(); 757 | 758 | base.OnDestroy(); 759 | } 760 | 761 | /// 762 | /// Creates a new copy of the currently selected entity. 763 | /// 764 | /// New entity. 765 | private Entity CreateEntity() 766 | { 767 | // Create new entity. 768 | ObjectData componentData = EntityManager.GetComponentData(_selectedEntity); 769 | Entity newEntity = EntityManager.CreateEntity(componentData.m_Archetype); 770 | 771 | // Set prefab and transform. 772 | EntityManager.SetComponentData(newEntity, new PrefabRef(_selectedEntity)); 773 | 774 | // Set tree growth to adult if this is a tree. 775 | if (EntityManager.HasComponent(newEntity)) 776 | { 777 | Tree treeData = new () 778 | { 779 | m_State = GetTreeState(), 780 | m_Growth = 0, 781 | }; 782 | 783 | EntityManager.SetComponentData(newEntity, treeData); 784 | } 785 | 786 | return newEntity; 787 | } 788 | 789 | /// 790 | /// Reduces XP gain for the current object to zero and records the original value. 791 | /// 792 | private void SaveXP() 793 | { 794 | // Reduce any XP to zero while we're using the tool. 795 | if (EntityManager.TryGetComponent(_selectedEntity, out PlaceableObjectData placeableData)) 796 | { 797 | _originalXP = placeableData.m_XPReward; 798 | placeableData.m_XPReward = 0; 799 | EntityManager.SetComponentData(_selectedEntity, placeableData); 800 | } 801 | else 802 | { 803 | _originalXP = 0; 804 | } 805 | } 806 | 807 | /// 808 | /// Restores the selected prefab's original XP gain. 809 | /// 810 | private void RestoreXP() 811 | { 812 | // Restore original prefab XP, if we changed it. 813 | if (_originalXP != 0 && _selectedEntity != Entity.Null && EntityManager.TryGetComponent(_selectedEntity, out PlaceableObjectData placeableData)) 814 | { 815 | placeableData.m_XPReward = _originalXP; 816 | EntityManager.SetComponentData(_selectedEntity, placeableData); 817 | _originalXP = 0; 818 | } 819 | } 820 | 821 | /// 822 | /// Gets the effective object rotation depending on current settings. 823 | /// 824 | /// Object position (to seed random number generator). 825 | /// Effective rotation quaternion according to current settings. 826 | private quaternion GetEffectiveRotation(float3 position) 827 | { 828 | int rotation = _rotation; 829 | 830 | // Override fixed rotation with a random value if we're using random rotation. 831 | if (_randomRotation) 832 | { 833 | // Use position to init RNG. 834 | _random.InitState((uint)(math.abs(position.x) + math.abs(position.y) + math.abs(position.z)) * 10000); 835 | rotation = _random.NextInt(360); 836 | } 837 | 838 | // Generate return quaternion. 839 | return quaternion.Euler(0, math.radians(rotation), 0); 840 | } 841 | 842 | /// 843 | /// Ensures any previewed trees have the correct age group. 844 | /// This resolves an issue where previewed trees will have their age group reset if they ever get blocked while previewing. 845 | /// 846 | /// Entity to check. 847 | private void EnsureTreeState(Entity entity) 848 | { 849 | // Ensure any trees have been assigned the correct age. 850 | if (EntityManager.TryGetComponent(entity, out Tree tree)) 851 | { 852 | tree.m_State = GetTreeState(); 853 | tree.m_Growth = 0; 854 | EntityManager.SetComponentData(entity, tree); 855 | } 856 | } 857 | 858 | /// 859 | /// Gets the tree state to apply to the next created tree. 860 | /// Uses Tree Controller to determine this if available, otherwise returns . 861 | /// 862 | /// Tree state to apply. 863 | private TreeState GetTreeState() 864 | { 865 | if (_treeControllerTool is null) 866 | { 867 | // Use this if Tree Controller is unavailable. 868 | return TreeState.Adult; 869 | } 870 | else 871 | { 872 | // Tree controller state. 873 | return (TreeState)_nextTreeState.GetValue(_treeControllerTool); 874 | } 875 | } 876 | 877 | /// 878 | /// Resets a tree to current tree state settings. 879 | /// 880 | /// Tree entity. 881 | private void ResetTreeState(Entity entity) 882 | { 883 | if (entity != Entity.Null) 884 | { 885 | if (EntityManager.TryGetComponent(entity, out Tree tree)) 886 | { 887 | tree.m_State = GetTreeState(); 888 | EntityManager.SetComponentData(entity, tree); 889 | } 890 | } 891 | } 892 | } 893 | } 894 | -------------------------------------------------------------------------------- /Code/Systems/LineToolTooltipSystem.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | namespace LineTool 8 | { 9 | using System.Collections.Generic; 10 | using Game.UI.Tooltip; 11 | using Game.UI.Widgets; 12 | using Unity.Collections; 13 | using Unity.Mathematics; 14 | using static Game.Rendering.GuideLinesSystem; 15 | 16 | /// 17 | /// The Line Tool tooltip system. 18 | /// 19 | public partial class LineToolTooltipSystem : TooltipSystemBase 20 | { 21 | private LineToolSystem _lineToolSystem; 22 | private List _tooltipGroups; 23 | 24 | /// 25 | /// Called when the system is created. 26 | /// 27 | protected override void OnCreate() 28 | { 29 | base.OnCreate(); 30 | 31 | _lineToolSystem = World.GetOrCreateSystemManaged(); 32 | _tooltipGroups = new List(); 33 | } 34 | 35 | /// 36 | /// Called every tool update. 37 | /// 38 | protected override void OnUpdate() 39 | { 40 | // Iterate through all tooltips in buffer. 41 | NativeList tooltips = _lineToolSystem.Tooltips; 42 | for (int i = 0; i < tooltips.Length; ++i) 43 | { 44 | // Create new tooltip template and add to list if needed. 45 | TooltipInfo tooltipInfo = tooltips[i]; 46 | if (_tooltipGroups.Count <= i) 47 | { 48 | _tooltipGroups.Add(new TooltipGroup 49 | { 50 | path = $"guideLineTooltip{i}", 51 | horizontalAlignment = TooltipGroup.Alignment.Center, 52 | verticalAlignment = TooltipGroup.Alignment.Center, 53 | children = { (IWidget)new IntTooltip() }, 54 | }); 55 | } 56 | 57 | // Set tooltip position. 58 | TooltipGroup tooltipGroup = _tooltipGroups[i]; 59 | float2 tooltipPos = TooltipSystemBase.WorldToTooltipPos(tooltipInfo.m_Position); 60 | if (!tooltipGroup.position.Equals(tooltipPos)) 61 | { 62 | tooltipGroup.position = tooltipPos; 63 | tooltipGroup.SetChildrenChanged(); 64 | } 65 | 66 | // Set tooltip content. 67 | IntTooltip intTooltip = tooltipGroup.children[0] as IntTooltip; 68 | switch (tooltipInfo.m_Type) 69 | { 70 | case TooltipType.Angle: 71 | intTooltip.icon = "Media/Glyphs/Angle.svg"; 72 | intTooltip.value = tooltipInfo.m_IntValue; 73 | intTooltip.unit = "angle"; 74 | break; 75 | case TooltipType.Length: 76 | intTooltip.icon = "Media/Glyphs/Length.svg"; 77 | intTooltip.value = tooltipInfo.m_IntValue; 78 | intTooltip.unit = "length"; 79 | break; 80 | } 81 | 82 | // Add tooltop group. to UI. 83 | AddGroup(tooltipGroup); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Code/Systems/LineToolUISystem.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | namespace LineTool 8 | { 9 | using System; 10 | using System.Collections.Generic; 11 | using System.IO; 12 | using System.Reflection; 13 | using System.Text; 14 | using cohtml.Net; 15 | using Colossal.Logging; 16 | using Game.Prefabs; 17 | using Game.SceneFlow; 18 | using Game.Tools; 19 | using Game.UI; 20 | using Unity.Entities; 21 | 22 | /// 23 | /// A tool UI system for LineTool. 24 | /// 25 | public sealed partial class LineToolUISystem : UISystemBase 26 | { 27 | // Cached references. 28 | private View _uiView; 29 | private ToolSystem _toolSystem; 30 | private LineToolSystem _lineToolSystem; 31 | private ILog _log; 32 | 33 | // Internal status. 34 | private bool _toolIsActive = false; 35 | private bool _activateTool = false; 36 | private bool _restorePreviousTool = false; 37 | private ToolBaseSystem _previousSystem = null; 38 | 39 | // Event binding. 40 | private List _eventHandles; 41 | 42 | // UI injection data. 43 | private string _injectedHTML; 44 | private string _injectedJS; 45 | private string _injectedCSS; 46 | private string _modeHTML; 47 | private string _modeJS; 48 | private string _commonJS; 49 | 50 | /// 51 | /// Updates the displayed spacing amount. 52 | /// 53 | internal void UpdateSpacing() 54 | { 55 | // Multiply spacing by 10 for accuracy conversion) 56 | ExecuteScript(_uiView, $"if (typeof(lineTool) == 'object') {{ lineTool.spacing = {_lineToolSystem.Spacing * 10}; if (lineTool.refreshSpacing) lineTool.refreshSpacing();}}"); 57 | } 58 | 59 | /// 60 | /// Clears any displayed tooltip. 61 | /// 62 | internal void ClearTooltip() 63 | { 64 | ExecuteScript(_uiView, "if (typeof(lineTool) == 'object') {{ lineTool.hideTooltip(); }}"); 65 | } 66 | 67 | /// 68 | /// Called when the system is created. 69 | /// 70 | protected override void OnCreate() 71 | { 72 | base.OnCreate(); 73 | 74 | // Set log. 75 | _log = Mod.Instance.Log; 76 | 77 | // Set references. 78 | _uiView = GameManager.instance.userInterface.view.View; 79 | _toolSystem = World.GetOrCreateSystemManaged(); 80 | _lineToolSystem = World.GetOrCreateSystemManaged(); 81 | 82 | // Read injection data. 83 | _commonJS = ReadJS("LineToolLite.UI.common.js"); 84 | _modeHTML = ReadHTML("LineToolLite.UI.modes.html", "modeDiv", "if (!document.getElementById(\"line-tool-modes\") && !document.getElementById(\"line-tool-panel\")) { lineTool.modeParent = document.getElementsByClassName(\"tool-options-panel_Se6\"); if (lineTool.modeParent.length != 0) lineTool.modeParent[0].appendChild(lineTool.modeDiv); }"); 85 | _modeJS = ReadJS("LineToolLite.UI.modes.js"); 86 | _injectedHTML = ReadHTML("LineToolLite.UI.ui.html", "div", "lineTool.div.className = \"tool-options-panel_Se6\"; lineTool.div.id = \"line-tool-panel\"; lineTool.targetParent = document.getElementsByClassName(\"tool-side-column_l9i\"); if (lineTool.targetParent.length == 0) lineTool.targetParent = document.getElementsByClassName(\"main_k4u\"); if (lineTool.targetParent.length != 0) lineTool.targetParent[0].appendChild(lineTool.div);"); 87 | _injectedJS = ReadJS("LineToolLite.UI.ui.js"); 88 | _injectedCSS = ReadCSS("LineToolLite.UI.ui.css"); 89 | 90 | // Initialize event handle list. 91 | _eventHandles = new (); 92 | 93 | _toolSystem.EventPrefabChanged = (Action)Delegate.Combine(_toolSystem.EventPrefabChanged, new Action(OnPrefabChanged)); 94 | } 95 | 96 | /// 97 | /// Called every UI update. 98 | /// 99 | protected override void OnUpdate() 100 | { 101 | base.OnUpdate(); 102 | 103 | // Check for tool activation trigger. 104 | if (_activateTool) 105 | { 106 | // Trigger set - clear it and activate tool. 107 | _activateTool = false; 108 | if (_toolSystem.activeTool != _lineToolSystem) 109 | { 110 | _log.Debug("enabling tool"); 111 | _lineToolSystem.EnableTool(); 112 | return; 113 | } 114 | } 115 | 116 | // Check for previous tool restoration trigger. 117 | if (_restorePreviousTool) 118 | { 119 | // Trigger set - clear it and restore previous tool. 120 | _restorePreviousTool = false; 121 | if (_toolSystem.activeTool == _lineToolSystem) 122 | { 123 | _log.Debug("restoring previous tool"); 124 | _lineToolSystem.RestorePreviousTool(); 125 | return; 126 | } 127 | } 128 | 129 | // Check for line tool activation. 130 | if (_toolSystem.activeTool == _lineToolSystem) 131 | { 132 | // Activate tool. 133 | if (!_toolIsActive) 134 | { 135 | // Tool is now active but previously wasn't; update previous tool system record. 136 | _previousSystem = _lineToolSystem; 137 | 138 | // Ensure JS setup. 139 | ExecuteScript(_uiView, _commonJS); 140 | 141 | // Set initial rotation and offset variables in UI (multiply distances by 10 for accuracy conversion). 142 | ExecuteScript(_uiView, $"lineTool.rotation = {_lineToolSystem.Rotation}; lineTool.randomSpacing = {_lineToolSystem.RandomSpacing * 10}; lineTool.randomOffset = {_lineToolSystem.RandomOffset * 10};"); 143 | 144 | // Attach our custom controls. 145 | // Inject scripts. 146 | _log.Debug("injecting component data"); 147 | ExecuteScript(_uiView, _injectedCSS); 148 | ExecuteScript(_uiView, _injectedHTML); 149 | ExecuteScript(_uiView, _injectedJS); 150 | 151 | // Determine active tool mode. 152 | string modeElement = _lineToolSystem.Mode switch 153 | { 154 | LineMode.SimpleCurve => "line-tool-simplecurve", 155 | LineMode.Circle => "line-tool-circle", 156 | _ => "line-tool-straight", 157 | }; 158 | 159 | // Select active tool button. 160 | ExecuteScript(_uiView, $"document.getElementById(\"{modeElement}\").classList.add(\"selected\");"); 161 | 162 | // Select random rotation button if needed. 163 | if (_lineToolSystem.RandomRotation) 164 | { 165 | ExecuteScript(_uiView, $"document.getElementById(\"line-tool-rotation-random\").classList.add(\"selected\");"); 166 | 167 | // Hide rotation buttons. 168 | ExecuteScript(_uiView, "lineTool.setRotationVisibility(false);"); 169 | } 170 | 171 | // Select fence mode button if needed and update visibility states. 172 | if (_lineToolSystem.CurrentSpacingMode == SpacingMode.FenceMode) 173 | { 174 | ExecuteScript(_uiView, $"document.getElementById(\"line-tool-fence\").classList.add(\"selected\"); lineTool.setFenceVisibility(false);"); 175 | } 176 | else if (_lineToolSystem.CurrentSpacingMode == SpacingMode.FullLength) 177 | { 178 | // Otherwise, select fixed-length even spacing button if needed. 179 | ExecuteScript(_uiView, $"document.getElementById(\"line-tool-measure-even\").classList.add(\"selected\");"); 180 | } 181 | 182 | // Show tree control menu if tree control is active. 183 | if (EntityManager.HasComponent(_lineToolSystem.SelectedEntity)) 184 | { 185 | ExecuteScript(_uiView, "lineTool.addTreeControl();"); 186 | } 187 | 188 | // Set initial spacing. 189 | UpdateSpacing(); 190 | 191 | // Register event callbacks. 192 | _eventHandles.Add(_uiView.RegisterForEvent("SetLineToolFenceMode", (Action)SetFenceMode)); 193 | _eventHandles.Add(_uiView.RegisterForEvent("SetLineToolSpacing", (Action)SetSpacing)); 194 | _eventHandles.Add(_uiView.RegisterForEvent("SetLineToolMeasureEven", (Action)SetFixedLength)); 195 | _eventHandles.Add(_uiView.RegisterForEvent("SetLineToolRandomRotation", (Action)SetRandomRotation)); 196 | _eventHandles.Add(_uiView.RegisterForEvent("SetLineToolRotation", (Action)SetRotation)); 197 | _eventHandles.Add(_uiView.RegisterForEvent("SetLineToolRandomSpacing", (Action)SetRandomSpacing)); 198 | _eventHandles.Add(_uiView.RegisterForEvent("SetLineToolRandomOffset", (Action)SetRandomOffset)); 199 | _eventHandles.Add(_uiView.RegisterForEvent("LineToolTreeControlUpdated", (Action)TreeControlUpdated)); 200 | _eventHandles.Add(_uiView.RegisterForEvent("SetLineToolSpacing", (Action)SetSpacing)); 201 | 202 | _eventHandles.Add(_uiView.RegisterForEvent("SetPointMode", (Action)SetPointMode)); 203 | _eventHandles.Add(_uiView.RegisterForEvent("SetStraightMode", (Action)SetStraightMode)); 204 | _eventHandles.Add(_uiView.RegisterForEvent("SetSimpleCurveMode", (Action)SetSimpleCurveMode)); 205 | _eventHandles.Add(_uiView.RegisterForEvent("SetCircleMode", (Action)SetCircleMode)); 206 | 207 | // Record current tool state. 208 | _toolIsActive = true; 209 | } 210 | } 211 | else 212 | { 213 | // Line tool not active - clean up if this is the first update after deactivation. 214 | if (_toolIsActive) 215 | { 216 | // Remove DOM activation. 217 | ExecuteScript(_uiView, "{ let panel = document.getElementById(\"line-tool-panel\"); if (panel) panel.parentElement.removeChild(panel); }"); 218 | 219 | // Remove event callbacks. 220 | foreach (BoundEventHandle eventHandle in _eventHandles) 221 | { 222 | _uiView.UnregisterFromEvent(eventHandle); 223 | } 224 | 225 | // Record current tool state. 226 | _toolIsActive = false; 227 | } 228 | else 229 | { 230 | // Check to see if another tool change has occurred. 231 | if (_toolSystem.activeTool != _previousSystem) 232 | { 233 | // Active tool has changed - record new tool. 234 | _previousSystem = _toolSystem.activeTool; 235 | 236 | // Check for object tool system activation. 237 | if (_previousSystem is ObjectToolSystem) 238 | { 239 | // Object tool is now active. 240 | _log.Debug("object tool system activated"); 241 | 242 | // Attach our custom controls. 243 | // Inject scripts. 244 | _log.Debug("injecting component data"); 245 | ExecuteScript(_uiView, _commonJS); 246 | ExecuteScript(_uiView, _modeHTML); 247 | ExecuteScript(_uiView, _modeJS); 248 | 249 | _eventHandles.Add(_uiView.RegisterForEvent("SetPointMode", (Action)SetPointMode)); 250 | _eventHandles.Add(_uiView.RegisterForEvent("SetStraightMode", (Action)SetStraightMode)); 251 | _eventHandles.Add(_uiView.RegisterForEvent("SetSimpleCurveMode", (Action)SetSimpleCurveMode)); 252 | _eventHandles.Add(_uiView.RegisterForEvent("SetCircleMode", (Action)SetCircleMode)); 253 | } 254 | else 255 | { 256 | // Remove any stale modes panel. 257 | ExecuteScript(_uiView, "{ let modePanel = document.getElementById(\"line-tool-modes\"); if (modePanel) modePanel.parentElement.removeChild(modePanel); }"); 258 | } 259 | } 260 | } 261 | } 262 | } 263 | 264 | /// 265 | /// Handles changes in the selected prefab. 266 | /// 267 | /// New selected prefab. 268 | private void OnPrefabChanged(PrefabBase prefab) 269 | { 270 | // If the line tool is currently activated and the new prefab is a placeable object, reactivate it (the game will reset the tool to the relevant object tool). 271 | if (_toolSystem.activeTool == _lineToolSystem && prefab is StaticObjectPrefab) 272 | { 273 | _activateTool = true; 274 | } 275 | } 276 | 277 | /// 278 | /// Executes JavaScript in the given View. 279 | /// 280 | /// to execute in. 281 | /// Script to execute. 282 | private void ExecuteScript(View view, string script) 283 | { 284 | // Null check. 285 | if (!string.IsNullOrEmpty(script)) 286 | { 287 | view?.ExecuteScript(script); 288 | } 289 | } 290 | 291 | /// 292 | /// Load CSS from an embedded UI file. 293 | /// 294 | /// Embedded UI file name to read. 295 | /// JavaScript embedding the CSS (null if empty or error). 296 | private string ReadCSS(string fileName) 297 | { 298 | try 299 | { 300 | // Attempt to read file. 301 | string css = ReadUIFile(fileName); 302 | 303 | // Don't do anything if file wasn't read. 304 | if (!string.IsNullOrEmpty(css)) 305 | { 306 | // Return JavaScript code with CSS embedded. 307 | return $"lineTool.style = document.createElement('style'); lineTool.style.type = 'text/css'; lineTool.style.innerHTML = \"{EscapeToJavaScript(css)}\"; document.head.appendChild(lineTool.style);"; 308 | } 309 | } 310 | catch (Exception e) 311 | { 312 | _log.Error(e, $"exception reading CSS file {fileName}"); 313 | } 314 | 315 | // If we got here, something went wrong.; return null. 316 | _log.Error($"failed to read embedded CSS file {fileName}"); 317 | return null; 318 | } 319 | 320 | /// 321 | /// Load HTML from an embedded UI file. 322 | /// 323 | /// Embedded UI file name to read. 324 | /// JavaScript variable name to use for the div. 325 | /// Injection JavaScript postfix text. 326 | /// JavaScript embedding the HTML (null if empty or error). 327 | private string ReadHTML(string fileName, string variableName, string injectionPostfix) 328 | { 329 | try 330 | { 331 | // Attempt to read file. 332 | string html = ReadUIFile(fileName); 333 | 334 | // Don't do anything if file wasn't read. 335 | if (!string.IsNullOrEmpty(html)) 336 | { 337 | // Return JavaScript code with HTML embedded. 338 | return $"lineTool.{variableName} = document.createElement('div'); lineTool.{variableName}.innerHTML = \"{EscapeToJavaScript(html)}\"; {injectionPostfix}"; 339 | } 340 | } 341 | catch (Exception e) 342 | { 343 | _log.Error(e, $"exception reading embedded HTML file {fileName}"); 344 | } 345 | 346 | // If we got here, something went wrong.; return null. 347 | _log.Error($"failed to read embedded HTML file {fileName}"); 348 | return null; 349 | } 350 | 351 | /// 352 | /// Load JavaScript from an embedded UI file. 353 | /// > 354 | /// UI file name to read. 355 | /// JavaScript as (null if empty or error). 356 | private string ReadJS(string fileName) 357 | { 358 | try 359 | { 360 | // Attempt to read file. 361 | string js = ReadUIFile(fileName); 362 | 363 | // Don't do anything if file wasn't read. 364 | if (!string.IsNullOrEmpty(js)) 365 | { 366 | // Return JavaScript code with HTML embedded. 367 | return js; 368 | } 369 | } 370 | catch (Exception e) 371 | { 372 | _log.Error(e, $"exception reading embedded JavaScript file {fileName}"); 373 | } 374 | 375 | // If we got here, something went wrong; return null. 376 | _log.Error($"failed to read embedded JavaScript file {fileName}"); 377 | return null; 378 | } 379 | 380 | /// 381 | /// Reads an embedded UI text file. 382 | /// 383 | /// Embedded UI file name to read. 384 | /// File contents (null if none or error). 385 | private string ReadUIFile(string fileName) 386 | { 387 | try 388 | { 389 | // Read file. 390 | using Stream embeddedStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(fileName); 391 | using StreamReader reader = new (embeddedStream); 392 | { 393 | return reader.ReadToEnd(); 394 | } 395 | } 396 | catch (Exception e) 397 | { 398 | _log.Error(e, $"exception reading embedded UI file {fileName}"); 399 | } 400 | 401 | return null; 402 | } 403 | 404 | /// 405 | /// Escapes HTML or CSS input for in-lining into JavaScript. 406 | /// 407 | /// HTML source. 408 | /// Escaped HTML as . 409 | private string EscapeToJavaScript(string sourceString) 410 | { 411 | // Create output StringBuilder. 412 | int length = sourceString.Length; 413 | StringBuilder stringBuilder = new (length * 2); 414 | 415 | // Iterate through each char. 416 | int index = -1; 417 | while (++index < length) 418 | { 419 | char ch = sourceString[index]; 420 | 421 | // Just skip line breaks. 422 | if (ch == '\n' || ch == '\r') 423 | { 424 | continue; 425 | } 426 | 427 | // Escape any double or single quotes. 428 | if (ch == '"' || ch == '\'') 429 | { 430 | stringBuilder.Append('\\'); 431 | } 432 | 433 | // Add character to output. 434 | stringBuilder.Append(ch); 435 | } 436 | 437 | return stringBuilder.ToString(); 438 | } 439 | 440 | /// 441 | /// Event callback to set fence mode. 442 | /// 443 | /// Value to set. 444 | private void SetFenceMode(bool isActive) => _lineToolSystem.CurrentSpacingMode = isActive ? SpacingMode.FenceMode : SpacingMode.Manual; 445 | 446 | /// 447 | /// Event callback to set single item mode. 448 | /// 449 | private void SetPointMode() 450 | { 451 | // Restore previously-used tool. 452 | _restorePreviousTool = true; 453 | } 454 | 455 | /// 456 | /// Event callback to set straight line mode. 457 | /// 458 | private void SetStraightMode() 459 | { 460 | // Ensure tool is activated. 461 | _activateTool = true; 462 | _lineToolSystem.Mode = LineMode.Straight; 463 | } 464 | 465 | /// 466 | /// Event callback to set simple curve mode. 467 | /// 468 | private void SetSimpleCurveMode() 469 | { 470 | // Ensure tool is activated. 471 | _activateTool = true; 472 | _lineToolSystem.Mode = LineMode.SimpleCurve; 473 | } 474 | 475 | /// 476 | /// Event callback to set circle mode. 477 | /// 478 | private void SetCircleMode() 479 | { 480 | // Ensure tool is activated. 481 | _activateTool = true; 482 | _lineToolSystem.Mode = LineMode.Circle; 483 | } 484 | 485 | /// 486 | /// Event callback to set current spacing. 487 | /// 488 | /// Value to set. 489 | private void SetSpacing(float spacing) => _lineToolSystem.Spacing = spacing; 490 | 491 | /// 492 | /// Event callback to set fixed-length even spacing mode. 493 | /// 494 | /// Value to set. 495 | private void SetFixedLength(bool isActive) => _lineToolSystem.CurrentSpacingMode = isActive ? SpacingMode.FullLength : SpacingMode.Manual; 496 | 497 | /// 498 | /// Event callback to set the random rotation override. 499 | /// 500 | /// Value to set. 501 | private void SetRandomRotation(bool isRandom) => _lineToolSystem.RandomRotation = isRandom; 502 | 503 | /// 504 | /// Event callback to set current rotation. 505 | /// 506 | /// Value to set. 507 | private void SetRotation(int rotation) => _lineToolSystem.Rotation = rotation; 508 | 509 | /// 510 | /// Event callback to set the random spacing offset maximum. 511 | /// 512 | /// Value to set. 513 | private void SetRandomSpacing(float randomSpacing) => _lineToolSystem.RandomSpacing = randomSpacing; 514 | 515 | /// 516 | /// Event callback to set the random lateral offset maximum. 517 | /// 518 | /// Value to set. 519 | private void SetRandomOffset(float randomOffset) => _lineToolSystem.RandomOffset = randomOffset; 520 | 521 | /// 522 | /// Event callback to update Tree Control settings. 523 | /// 524 | private void TreeControlUpdated() => _lineToolSystem.RefreshTreeControl(); 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /Config/PostBuild.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net472 5 | 2022.3.7f1 6 | $(LOCALAPPDATA)\..\LocalLow\Colossal Order\Cities Skylines II 7 | $(MSBuildProgramFiles32)\Steam\steamapps\common\Cities Skylines II 8 | $(InstallationPath)\Cities2_Data\Managed 9 | 10 | $(AssemblySearchPaths); 11 | $(ManagedDLLPath); 12 | 13 | 14 | -------------------------------------------------------------------------------- /Config/References.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | false 9 | 10 | 11 | false 12 | 13 | 14 | false 15 | 16 | 17 | false 18 | 19 | 20 | false 21 | 22 | 23 | false 24 | 25 | 26 | false 27 | 28 | 29 | false 30 | 31 | 32 | false 33 | 34 | 35 | false 36 | 37 | 38 | false 39 | 40 | 41 | false 42 | 43 | 44 | false 45 | 46 | 47 | false 48 | 49 | 50 | false 51 | 52 | 53 | -------------------------------------------------------------------------------- /Config/Targets.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | $(InstallationPath)/BepInEx/plugins/$(ProjectName) 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | using System.Diagnostics.CodeAnalysis; 8 | 9 | [assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1308:Variable names should not be prefixed", Justification = "Follow dotnet/runtime coding style")] 10 | [assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "Follow dotnet/runtime coding style")] 11 | [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "Follow dotnet/runtime coding style")] 12 | [assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Follow game coding style")] 13 | -------------------------------------------------------------------------------- /Icons/Circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Icons/Dice.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Icons/Fence.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Icons/MeasureEven.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LineToolLite.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Line Tool Lite 4 | $(Title) 5 | A Cities: Skylines 2 mod. 6 | algernon 7 | Copyright © 2023-24 algernon (github.com/algernon-A). All rights reserved. 8 | $(Title) 9 | 1.3.4 10 | 9.0 11 | True 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | all 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /LineToolLite.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34322.80 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LineToolLite", "LineToolLite.csproj", "{3923F136-38EF-4AA8-8C20-C27FF001F443}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {3923F136-38EF-4AA8-8C20-C27FF001F443}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {3923F136-38EF-4AA8-8C20-C27FF001F443}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {3923F136-38EF-4AA8-8C20-C27FF001F443}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {3923F136-38EF-4AA8-8C20-C27FF001F443}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {1D6BCC73-E9A7-4BFD-89E5-41D8443FC370} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Line Tool Lite 2 | github.com/Algernon-A/LineToolLite 3 | Copyright (c) 2023-24 algernon (K. Algernon A. Sheppard). 4 | 5 | A modified version of the full Line Tool mod for Cities: Skylines II (github.com/Algernon-A/LineTool-CS2) with alterations and removals to make it compatible with BepInEx and usable with Cities: Skylines public builds from version 1.0.14 which do not have official modding support. 6 | Copyright (c) 2023-24 algernon (K. Algernon A. Sheppard; github.com/Algernon-A). All rights reserved. 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## So long and thanks for all the fish! 2 | With the launch of Paradox Mods and the addition of official modding support to Cities: Skylines 2, the time for this experimental version of the Line Tool mod has now come to a close. 3 | 4 | Simply put, this is now deprecated, and probably doesn't even work any more. 5 | 6 | This mod has been replaced by the full version of Line Tool, which is [available now on Paradox Mods](https://mods.paradoxplaza.com/mods/75816/Windows). 7 | -------------------------------------------------------------------------------- /UI/common.js: -------------------------------------------------------------------------------- 1 | // Ensure container. 2 | if (typeof lineTool != 'object') var lineTool = {}; 3 | 4 | // Function to setup buttons. 5 | if (typeof lineTool.setupClickButton2 !== 'function') { 6 | lineTool.setupClickButton2 = function (id, onclick, toolTipKey) { 7 | let newButton = document.getElementById(id); 8 | if (newButton) { 9 | newButton.onclick = onclick; 10 | } 11 | } 12 | } 13 | 14 | // Function to set div visibility 15 | if (typeof lineTool.setDivVisiblity !== 'function') { 16 | lineTool.setDivVisiblity = function (isVisible, divId) { 17 | if (isVisible) { 18 | document.getElementById(divId).style.visibility = "visible"; 19 | } 20 | else { 21 | document.getElementById(divId).style.visibility = "hidden"; 22 | } 23 | } 24 | } 25 | 26 | // Function to set the visibility status of a button with icon child. 27 | if (typeof lineTool.setButtonVisibility !== 'function') { 28 | lineTool.setButtonVisibility = function (button, isVisible) { 29 | var firstChild = button.firstChild; 30 | if (isVisible) { 31 | button.classList.remove("hidden"); 32 | firstChild.classList.remove("hidden"); 33 | firstChild.style.display = "inline"; 34 | } 35 | else { 36 | button.classList.add("hidden"); 37 | firstChild.classList.add("hidden"); 38 | firstChild.style.display = "none"; 39 | } 40 | } 41 | } 42 | 43 | // Function to apply translation strings. 44 | if (typeof lineTool.applyLocalization !== 'function') { 45 | lineTool.applyLocalization = function (target) { 46 | if (!target) { 47 | return; 48 | } 49 | 50 | let targets = target.querySelectorAll('[localeKey]'); 51 | targets.forEach(function (currentValue) { 52 | currentValue.innerHTML = engine.translate(currentValue.getAttribute("localeKey")); 53 | }); 54 | } 55 | } 56 | 57 | // Function to setup buttons. 58 | if (typeof lineTool.setupClickButton !== 'function') { 59 | lineTool.setupClickButton = function (id, onclick, toolTipKey) { 60 | let newButton = document.getElementById(id); 61 | if (newButton) { 62 | newButton.onclick = onclick; 63 | lineTool.setTooltip(id, toolTipKey); 64 | } 65 | } 66 | } 67 | 68 | // Function to setup controls with a scrollwheel component. 69 | if (typeof lineTool.setupWheel !== 'function') { 70 | lineTool.setupWheel = function (id, onwheel) { 71 | let newControl = document.getElementById(id); 72 | if (newControl) { 73 | newControl.onwheel = onwheel; 74 | } 75 | } 76 | } 77 | 78 | // Function to setup tooltip. 79 | if (typeof lineTool.setTooltip !== 'function') { 80 | lineTool.setTooltip = function (id, toolTipKey) { 81 | let target = document.getElementById(id); 82 | target.onmouseenter = () => lineTool.showTooltip(document.getElementById(id), toolTipKey); 83 | target.onmouseleave = lineTool.hideTooltip; 84 | } 85 | } 86 | 87 | // Function to show a tooltip, creating if necessary. 88 | if (typeof lineTool.showTooltip !== 'function') { 89 | lineTool.showTooltip = function (parent, tooltipKey) { 90 | 91 | if (!lineTool.tooltip) { 92 | lineTool.tooltip = document.createElement("div"); 93 | lineTool.tooltip.style.visibility = "hidden"; 94 | lineTool.tooltip.classList.add("balloon_qJY", "balloon_H23", "up_ehW", "center_hug", "anchored-balloon_AYp", "up_el0"); 95 | let boundsDiv = document.createElement("div"); 96 | boundsDiv.classList.add("bounds__AO"); 97 | let containerDiv = document.createElement("div"); 98 | containerDiv.classList.add("container_zgM", "container_jfe"); 99 | let contentDiv = document.createElement("div"); 100 | contentDiv.classList.add("content_A82", "content_JQV"); 101 | let arrowDiv = document.createElement("div"); 102 | arrowDiv.classList.add("arrow_SVb", "arrow_Xfn"); 103 | let broadDiv = document.createElement("div"); 104 | lineTool.tooltipTitle = document.createElement("div"); 105 | lineTool.tooltipTitle.classList.add("title_lCJ"); 106 | let paraDiv = document.createElement("div"); 107 | paraDiv.classList.add("paragraphs_nbD", "description_dNa"); 108 | lineTool.tooltipPara = document.createElement("p"); 109 | lineTool.tooltipPara.setAttribute("cohinline", "cohinline"); 110 | 111 | paraDiv.appendChild(lineTool.tooltipPara); 112 | broadDiv.appendChild(lineTool.tooltipTitle); 113 | broadDiv.appendChild(paraDiv); 114 | containerDiv.appendChild(arrowDiv); 115 | contentDiv.appendChild(broadDiv); 116 | boundsDiv.appendChild(containerDiv); 117 | boundsDiv.appendChild(contentDiv); 118 | lineTool.tooltip.appendChild(boundsDiv); 119 | 120 | // Append tooltip to screen element. 121 | let screenParent = document.getElementsByClassName("game-main-screen_TRK"); 122 | if (screenParent.length == 0) { 123 | screenParent = document.getElementsByClassName("editor-main-screen_m89"); 124 | } 125 | if (screenParent.length > 0) { 126 | screenParent[0].appendChild(lineTool.tooltip); 127 | } 128 | } 129 | 130 | // Set text and position. 131 | lineTool.tooltipTitle.innerHTML = engine.translate("LINETOOL." + tooltipKey); 132 | lineTool.tooltipPara.innerHTML = engine.translate("LINETOOL_DESCRIPTION." + tooltipKey); 133 | 134 | // Set visibility tracking to prevent race conditions with popup delay. 135 | lineTool.tooltipVisibility = "visible"; 136 | 137 | // Slightly delay popup by three frames to prevent premature activation and to ensure layout is ready. 138 | window.requestAnimationFrame(() => { 139 | window.requestAnimationFrame(() => { 140 | window.requestAnimationFrame(() => { 141 | lineTool.setTooltipPos(parent); 142 | }); 143 | 144 | }); 145 | }); 146 | } 147 | } 148 | 149 | // Function to adjust the position of a tooltip and make visible. 150 | if (typeof lineTool.setTooltipPos !== 'function') { 151 | lineTool.setTooltipPos = function (parent) { 152 | if (!lineTool.tooltip) { 153 | return; 154 | } 155 | 156 | let tooltipRect = lineTool.tooltip.getBoundingClientRect(); 157 | let parentRect = parent.getBoundingClientRect(); 158 | let xPos = parentRect.left + ((parentRect.width - tooltipRect.width) / 2); 159 | let yPos = parentRect.top - tooltipRect.height; 160 | lineTool.tooltip.setAttribute("style", "left:" + xPos + "px; top: " + yPos + "px; --posY: " + yPos + "px; --posX:" + xPos + "px"); 161 | 162 | lineTool.tooltip.style.visibility = lineTool.tooltipVisibility; 163 | } 164 | } 165 | 166 | // Function to hide the tooltip. 167 | if (typeof lineTool.hideTooltip !== 'function') { 168 | lineTool.hideTooltip = function () { 169 | if (lineTool.tooltip) { 170 | lineTool.tooltipVisibility = "hidden"; 171 | lineTool.tooltip.style.visibility = "hidden"; 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /UI/modes.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 8 | 11 | 14 | 17 |
18 |
19 |
-------------------------------------------------------------------------------- /UI/modes.js: -------------------------------------------------------------------------------- 1 | // Function to activate point mode. 2 | if (typeof lineTool.handlePointMode !== 'function') { 3 | lineTool.handlePointMode = function () { 4 | document.getElementById("line-tool-mode-simplecurve").classList.remove("selected"); 5 | document.getElementById("line-tool-mode-circle").classList.remove("selected"); 6 | document.getElementById("line-tool-mode-straight").classList.remove("selected"); 7 | document.getElementById("line-tool-mode-point").classList.add("selected"); 8 | engine.trigger('SetPointMode'); 9 | } 10 | } 11 | 12 | // Function to activate straight mode. 13 | if (typeof lineTool.handleStraightMode !== 'function') { 14 | lineTool.handleStraightMode = function () { 15 | document.getElementById("line-tool-mode-point").classList.remove("selected"); 16 | document.getElementById("line-tool-mode-simplecurve").classList.remove("selected"); 17 | document.getElementById("line-tool-mode-circle").classList.remove("selected"); 18 | document.getElementById("line-tool-mode-straight").classList.add("selected"); 19 | engine.trigger('SetStraightMode'); 20 | } 21 | } 22 | 23 | // Function to activate simple curve mode. 24 | if (typeof lineTool.handleSimpleCurveMode !== 'function') { 25 | lineTool.handleSimpleCurveMode = function () { 26 | document.getElementById("line-tool-mode-point").classList.remove("selected"); 27 | document.getElementById("line-tool-mode-straight").classList.remove("selected"); 28 | document.getElementById("line-tool-mode-circle").classList.remove("selected"); 29 | document.getElementById("line-tool-mode-simplecurve").classList.add("selected"); 30 | engine.trigger('SetSimpleCurveMode'); 31 | } 32 | } 33 | 34 | // Function to activate circle mode. 35 | if (typeof lineTool.handleCircleMode !== 'function') { 36 | lineTool.handleCircleMode = function () { 37 | document.getElementById("line-tool-mode-point").classList.remove("selected"); 38 | document.getElementById("line-tool-mode-straight").classList.remove("selected"); 39 | document.getElementById("line-tool-mode-simplecurve").classList.remove("selected"); 40 | document.getElementById("line-tool-mode-circle").classList.add("selected"); 41 | engine.trigger('SetCircleMode'); 42 | } 43 | } 44 | 45 | // Add button event handlers. 46 | lineTool.setupClickButton("line-tool-mode-point", lineTool.handlePointMode, "PointMode"); 47 | lineTool.setupClickButton("line-tool-mode-straight", lineTool.handleStraightMode, "StraightLine"); 48 | lineTool.setupClickButton("line-tool-mode-simplecurve", lineTool.handleSimpleCurveMode, "SimpleCurve"); 49 | lineTool.setupClickButton("line-tool-mode-circle", lineTool.handleCircleMode, "Circle"); 50 | 51 | // Apply translations. 52 | lineTool.applyLocalization(lineTool.modeDiv); -------------------------------------------------------------------------------- /UI/ui.css: -------------------------------------------------------------------------------- 1 | /* Simple component to hide its parent. */ 2 | .hidden { 3 | opacity: 0; 4 | pointer-events: none; 5 | background-color: #00000000; 6 | } 7 | -------------------------------------------------------------------------------- /UI/ui.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | 12 | 15 | 18 | 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 41 | 44 |
0 m
45 | 48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | 58 | 61 |
62 | 65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | 76 |
0 m
77 | 80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | 90 |
0 m
91 | 94 |
95 |
96 |
97 |
98 | 99 | -------------------------------------------------------------------------------- /UI/ui.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) algernon (K. Algernon A. Sheppard). All rights reserved. 3 | // Licensed under the Apache Licence, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 4 | // See LICENSE.txt file in the project root for full license information. 5 | // 6 | 7 | 8 | // Function to activate point mode. 9 | if (typeof lineTool.handlePoint !== 'function') { 10 | lineTool.handlePoint = function () { 11 | document.getElementById("line-tool-simplecurve").classList.remove("selected"); 12 | document.getElementById("line-tool-circle").classList.remove("selected"); 13 | document.getElementById("line-tool-straight").classList.remove("selected"); 14 | document.getElementById("line-tool-point").classList.add("selected"); 15 | engine.trigger('SetPointMode'); 16 | } 17 | } 18 | 19 | // Function to activate straight mode. 20 | if (typeof lineTool.handleStraight !== 'function') { 21 | lineTool.handleStraight = function () { 22 | document.getElementById("line-tool-point").classList.remove("selected"); 23 | document.getElementById("line-tool-simplecurve").classList.remove("selected"); 24 | document.getElementById("line-tool-circle").classList.remove("selected"); 25 | document.getElementById("line-tool-straight").classList.add("selected"); 26 | engine.trigger('SetStraightMode'); 27 | } 28 | } 29 | 30 | // Function to activate simple curve mode. 31 | if (typeof lineTool.handleSimpleCurve !== 'function') { 32 | lineTool.handleSimpleCurve = function () { 33 | document.getElementById("line-tool-point").classList.remove("selected"); 34 | document.getElementById("line-tool-straight").classList.remove("selected"); 35 | document.getElementById("line-tool-circle").classList.remove("selected"); 36 | document.getElementById("line-tool-simplecurve").classList.add("selected"); 37 | engine.trigger('SetSimpleCurveMode'); 38 | } 39 | } 40 | 41 | // Function to activate circle mode. 42 | if (typeof lineTool.handleCircle !== 'function') { 43 | lineTool.handleCircle = function () { 44 | document.getElementById("line-tool-point").classList.remove("selected"); 45 | document.getElementById("line-tool-straight").classList.remove("selected"); 46 | document.getElementById("line-tool-simplecurve").classList.remove("selected"); 47 | document.getElementById("line-tool-circle").classList.add("selected"); 48 | engine.trigger('SetCircleMode'); 49 | } 50 | } 51 | 52 | // Function to apply modifiers to distance adjustments. 53 | if (typeof lineTool.adjustDistance != 'function') { 54 | lineTool.adjustDistance = function (event, adjustment) { 55 | 56 | // Adjust for modifier keys. 57 | let finalAdjustment = adjustment; 58 | if (event) { 59 | if (event.shiftKey) 60 | finalAdjustment *= 90; 61 | else if (!event.ctrlKey) 62 | finalAdjustment *= 10; 63 | } 64 | 65 | return finalAdjustment; 66 | } 67 | } 68 | 69 | // Function to implement fence mode selection. 70 | if (typeof lineTool.fenceMode !== 'function') { 71 | lineTool.fenceMode = function () { 72 | let fenceModeButton = document.getElementById("line-tool-fence"); 73 | let activating = !fenceModeButton.classList.contains("selected"); 74 | if (activating) { 75 | fenceModeButton.classList.add("selected"); 76 | 77 | // Deselect random rotation. 78 | document.getElementById("line-tool-rotation-random").classList.remove("selected"); 79 | engine.trigger('SetLineToolRandomRotation', false); 80 | lineTool.setRotationVisibility(true); 81 | } 82 | else { 83 | fenceModeButton.classList.remove("selected"); 84 | } 85 | 86 | // Update control visibility. 87 | lineTool.setFenceVisibility(!activating); 88 | engine.trigger('SetLineToolFenceMode', activating); 89 | } 90 | } 91 | 92 | // Function to toggle visibility of controls based on fence mode state. 93 | if (typeof lineTool.setFenceVisibility !== 'function') { 94 | lineTool.setFenceVisibility = function (isVisible) { 95 | lineTool.setDivVisiblity(isVisible, "line-tool-spacing"); 96 | lineTool.setDivVisiblity(isVisible, "line-tool-rotation"); 97 | lineTool.setDivVisiblity(isVisible, "line-tool-rotation-field"); 98 | lineTool.setDivVisiblity(isVisible, "line-tool-offsets"); 99 | } 100 | } 101 | 102 | // Function to adjust spacing. 103 | if (typeof lineTool.adjustSpacing !== 'function') { 104 | lineTool.adjustSpacing = function (event, adjustment) { 105 | // Adjust for modifiers. 106 | let finalAdjustment = lineTool.adjustDistance(event, adjustment); 107 | 108 | // Don't apply if adjutment will bring us below zero. 109 | newSpacing = lineTool.spacing + finalAdjustment; 110 | if (newSpacing < 1) return; 111 | 112 | // Apply spacing. 113 | lineTool.spacing = newSpacing; 114 | let roundedSpacing = newSpacing / 10; 115 | engine.trigger('SetLineToolSpacing', roundedSpacing); 116 | document.getElementById("line-tool-spacing-field").innerHTML = roundedSpacing + " m"; 117 | } 118 | } 119 | 120 | // Function to update displayed spacing. 121 | if (typeof lineTool.refreshSpacing !== 'function') { 122 | lineTool.refreshSpacing = function () { 123 | if (lineTool.spacing == null) { 124 | return; 125 | } 126 | 127 | let spacingField = document.getElementById("line-tool-spacing-field"); 128 | if (spacingField != null) { 129 | document.getElementById("line-tool-spacing-field").innerHTML = (lineTool.spacing / 10) + " m"; 130 | } 131 | } 132 | } 133 | 134 | // Function to implement fixed-length even spacing. 135 | if (typeof lineTool.measureEven !== 'function') { 136 | lineTool.measureEven = function () { 137 | let measureEvenButton = document.getElementById("line-tool-measure-even"); 138 | if (measureEvenButton.classList.contains("selected")) { 139 | measureEvenButton.classList.remove("selected"); 140 | engine.trigger('SetLineToolMeasureEven', false); 141 | } 142 | else { 143 | measureEvenButton.classList.add("selected"); 144 | engine.trigger('SetLineToolMeasureEven', true); 145 | } 146 | } 147 | } 148 | 149 | // Function to implement random rotation selection. 150 | if (typeof lineTool.randomRotation !== 'function') { 151 | lineTool.randomRotation = function () { 152 | let randomRotationButton = document.getElementById("line-tool-rotation-random"); 153 | if (randomRotationButton.classList.contains("selected")) { 154 | randomRotationButton.classList.remove("selected"); 155 | engine.trigger('SetLineToolRandomRotation', false); 156 | 157 | // Show rotation tools. 158 | lineTool.setRotationVisibility(true); 159 | } 160 | else { 161 | randomRotationButton.classList.add("selected"); 162 | engine.trigger('SetLineToolRandomRotation', true); 163 | 164 | // Hide rotation tools. 165 | lineTool.setRotationVisibility(false); 166 | } 167 | } 168 | } 169 | 170 | // Function to adjust rotation. 171 | if (typeof lineTool.adjustRotation !== 'function') { 172 | lineTool.adjustRotation = function(event, adjustment) { 173 | // Adjust for modifier keys. 174 | let finalAdjustment = adjustment; 175 | if (event) { 176 | if (event.shiftKey) 177 | finalAdjustment *= 90; 178 | else if (!event.ctrlKey) 179 | finalAdjustment *= 10; 180 | } 181 | 182 | // Bounds check rotation. 183 | lineTool.rotation += finalAdjustment; 184 | if (lineTool.rotation >= 360) { 185 | lineTool.rotation -= 360; 186 | } 187 | if (lineTool.rotation < 0) { 188 | lineTool.rotation += 360; 189 | } 190 | 191 | // Apply rotation. 192 | engine.trigger('SetLineToolRotation', lineTool.rotation); 193 | document.getElementById("line-tool-rotation-field").innerHTML = lineTool.rotation + "°"; 194 | } 195 | } 196 | 197 | // Function to adjust random spacing offset. 198 | if (typeof lineTool.adjustRandomSpacing !== 'function') { 199 | lineTool.adjustRandomSpacing = function (event, adjustment) { 200 | // Adjust for modifiers. 201 | let finalAdjustment = lineTool.adjustDistance(event, adjustment); 202 | 203 | // Bounds check. 204 | lineTool.randomSpacing += finalAdjustment; 205 | let maxSpacing = Math.round((lineTool.spacing / 3) - 1); 206 | if (lineTool.randomSpacing > maxSpacing) { 207 | lineTool.randomSpacing = maxSpacing; 208 | } 209 | if (lineTool.randomSpacing < 0) { 210 | lineTool.randomSpacing = 0; 211 | } 212 | 213 | // Apply spacing offset. 214 | engine.trigger('SetLineToolRandomSpacing', lineTool.randomSpacing / 10); 215 | document.getElementById("line-tool-xOffset-field").innerHTML = (lineTool.randomSpacing / 10) + " m"; 216 | } 217 | } 218 | 219 | // Function to adjust random lateral offset. 220 | if (typeof lineTool.adjustRandomOffset !== 'function') { 221 | lineTool.adjustRandomOffset = function (event, adjustment) { 222 | // Adjust for modifiers. 223 | let finalAdjustment = lineTool.adjustDistance(event, adjustment); 224 | 225 | // Bounds check. 226 | lineTool.randomOffset += finalAdjustment; 227 | if (lineTool.randomOffset > 1000) { 228 | lineTool.randomOffset = 1000; 229 | } 230 | if (lineTool.randomOffset < 0) { 231 | lineTool.randomOffset = 0; 232 | } 233 | 234 | // Apply spacing offset. 235 | engine.trigger('SetLineToolRandomOffset', lineTool.randomOffset / 10); 236 | document.getElementById("line-tool-zOffset-field").innerHTML = (lineTool.randomOffset / 10) + " m"; 237 | } 238 | } 239 | 240 | // Function to show the Tree Control age panel. 241 | if (typeof lineTool.addTreeControl !== 'function') { 242 | lineTool.addTreeControl = function (event, adjustment) { 243 | try { 244 | if (typeof yyTreeController != 'undefined' && typeof yyTreeController.buildTreeAgeItem == 'function') { 245 | let modeLine = document.getElementById("line-tool-mode"); 246 | yyTreeController.buildTreeAgeItem(modeLine, "afterend"); 247 | document.getElementById("YYTC-change-age-buttons-panel").onclick = function () { engine.trigger('LineToolTreeControlUpdated') }; 248 | } 249 | } 250 | catch { 251 | // Don't do anything. 252 | } 253 | } 254 | } 255 | 256 | // Function to set rotation selection control visibility 257 | if (typeof lineTool.setRotationVisibility !== 'function') { 258 | lineTool.setRotationVisibility = function(isVisible) { 259 | lineTool.setButtonVisibility(document.getElementById("line-tool-rotation-up"), isVisible); 260 | lineTool.setButtonVisibility(document.getElementById("line-tool-rotation-down"), isVisible); 261 | if (isVisible) { 262 | document.getElementById("line-tool-rotation-field").style.visibility = "visible"; 263 | } 264 | else { 265 | document.getElementById("line-tool-rotation-field").style.visibility = "hidden"; 266 | } 267 | } 268 | } 269 | 270 | // Set initial figures. 271 | lineTool.adjustSpacing(null, 0); 272 | lineTool.adjustRotation(null, 0); 273 | lineTool.adjustRandomOffset(null, 0); 274 | lineTool.adjustRandomSpacing(null, 0); 275 | 276 | // Add button event handlers. 277 | lineTool.setupClickButton("line-tool-point", lineTool.handlePoint, "PointMode"); 278 | lineTool.setupClickButton("line-tool-straight", lineTool.handleStraight, "StraightLine"); 279 | lineTool.setupClickButton("line-tool-simplecurve", lineTool.handleSimpleCurve, "SimpleCurve"); 280 | lineTool.setupClickButton("line-tool-circle", lineTool.handleCircle, "Circle"); 281 | 282 | lineTool.setupClickButton("line-tool-fence", lineTool.fenceMode, "FenceMode"); 283 | 284 | lineTool.setupClickButton("line-tool-measure-even", lineTool.measureEven, "FixedLength"); 285 | lineTool.setupClickButton("line-tool-rotation-random", lineTool.randomRotation, "RandomRotation"); 286 | 287 | lineTool.setupClickButton("line-tool-spacing-down", (event) => { lineTool.adjustSpacing(event, -1); }, "SpacingDown"); 288 | lineTool.setupClickButton("line-tool-spacing-up", (event) => { lineTool.adjustSpacing(event, 1); }, "SpacingUp"); 289 | lineTool.setupClickButton("line-tool-rotation-down", (event) => { lineTool.adjustRotation(event, -1); }, "AntiClockwise"); 290 | lineTool.setupClickButton("line-tool-rotation-up", (event) => { lineTool.adjustRotation(event, 1); }, "Clockwise"); 291 | 292 | lineTool.setupClickButton("line-tool-xOffset-down", (event) => { lineTool.adjustRandomSpacing(event, -1); }, "RandomSpacingDown"); 293 | lineTool.setupClickButton("line-tool-xOffset-up", (event) => { lineTool.adjustRandomSpacing(event, 1); }, "RandomSpacingUp"); 294 | lineTool.setupClickButton("line-tool-zOffset-down", (event) => { lineTool.adjustRandomOffset(event, -1); }, "RandomOffsetDown"); 295 | lineTool.setupClickButton("line-tool-zOffset-up", (event) => { lineTool.adjustRandomOffset(event, 1); }, "RandomOffsetUp"); 296 | 297 | lineTool.setTooltip("line-tool-spacing-field", "Spacing"); 298 | lineTool.setTooltip("line-tool-rotation-field", "Rotation"); 299 | lineTool.setTooltip("line-tool-xOffset-field", "SpacingVariation"); 300 | lineTool.setTooltip("line-tool-zOffset-field", "OffsetVariation"); 301 | 302 | lineTool.setupWheel("line-tool-spacing-field", (event) => { lineTool.adjustSpacing(event, event.deltaY / 30); }); 303 | lineTool.setupWheel("line-tool-rotation-field", (event) => { lineTool.adjustRotation(event, event.deltaY / 30); }); 304 | lineTool.setupWheel("line-tool-xOffset-field", (event) => { lineTool.adjustRandomSpacing(event, event.deltaY / 30); }); 305 | lineTool.setupWheel("line-tool-zOffset-field", (event) => { lineTool.adjustRandomOffset(event, event.deltaY / 30); }); 306 | 307 | // Apply translations. 308 | lineTool.applyLocalization(lineTool.div); 309 | 310 | // Clear any stale tooltip reference. 311 | lineTool.tooltip = null; 312 | 313 | // Hide any existing modes panel. 314 | if (document.getElementById("line-tool-modes")) { 315 | let modePanel = document.getElementById("line-tool-modes"); 316 | if (modePanel.parentElement) { 317 | modePanel.parentElement.removeChild(modePanel); 318 | } 319 | } 320 | 321 | // Set panel additional class for editor. 322 | if (document.getElementsByClassName("editor-main-screen_m89").length > 0) { 323 | document.getElementById("line-tool-panel").classList.add("tool-options_Cqd"); 324 | } -------------------------------------------------------------------------------- /l10n.csv: -------------------------------------------------------------------------------- 1 | scope key en-US zh-HANS nl-NL de-DE ro-RO es-ES zh-HANT 2 | LINETOOL Title Line tool Line Tool Lijn penseel Linienwerkzeug Instrumentul pentru linii Line tool Line Tool 3 | LINETOOL LineMode Line mode 摆放模式 Lijn modus Linienmodus Mod „linie” Modo de línea 擺放模式 4 | LINETOOL Options Options 设置 Opties Einstellungen Opțiuni Opciones 設定 5 | LINETOOL FenceMode Fence mode 栅栏模式 Schutting modus Zaunmodus Mod „gard” Modo valla 柵欄模式 6 | LINETOOL_DESCRIPTION FenceMode Automatically aligns and places objects continuously, like a fence. 像栅栏一样,自动对齐和放置物体。 Lijnt en plaatst objecten automatisch continu uit, zoals een schutting. Platziert Objekte automatisch wie einen Zaun und richtet sie entsprechend aus. Aliniază automat și plasează obiectele continuu, ca un gard. Alinea y coloca objetos de forma continua automáticamente, como una valla. 自動對齊與放置物品,就像柵欄一樣。 7 | LINETOOL PointMode Single item 单个物体 Enkel object Einzelnes Objekt Element unic Un elemento 單件物品 8 | LINETOOL_DESCRIPTION PointMode Place one object at a time using the standard game tool. 使用游戏默认工具,一次只摆放一个物体。 Plaats één object tegelijkertijd met behulp van de standaard game tool. Platziere einzelne Objekt mit dem Standardwerkzeug. Plasează un obiect pe rând folosind instrumentul standard al jocului. Coloca un elemento a la vez usando la herramienta estándar del juego. 使用遊戲預設工具,一次僅擺放一個物品。 9 | LINETOOL StraightLine Straight line 直线 Rechte lijn Gerade Linie Linie dreaptă Línea recta 直線 10 | LINETOOL_DESCRIPTION StraightLine Place objects along a straight line from point A to point B. 将物体沿着从 A 点到 B 点的直线放置。 Plaats voorwerpen langs een rechte lijn van punt A naar punt B. Platziert Objekte entlang einer geraden Linie von Punkt A zu Punkt B. Plasează obiecte de-a lungul unei linii drepte de la punctul A la punctul B. Coloca objetos en línea del punto A al punto B. 從A點到B點以直線擺放物品。 11 | LINETOOL SimpleCurve Simple curve 简单曲线 Eenvoudige bocht Einfache Kurve Curbă simplă Curva simple 簡易曲線 12 | LINETOOL_DESCRIPTION SimpleCurve Define a start and bend, then define end point. 先设置起点和曲率,再设置终点。 Definieer een begin en daarna een bocht, definieer vervolgens een eindpunt. Definiere einen Start und eine Kurve, dann definiere den Endpunkt. Definește un început și o curbă, apoi definește punctul final. Define un comienzo y la curva, luego define el pinto final. 設定起點與曲率,最後設置終點。 13 | LINETOOL Circle Circle 圆圈 Cirkel Kreis Cerc Círculo 圓圈 14 | LINETOOL_DESCRIPTION Circle Define the center of the circle, then set the radius. 先设置圆心,再设置半径。 Definieer het middelpunt van de cirkel, stel vervolgens de straal in. Definiere die Mitte des Kreises und lege dann den Radius fest. Definește centrul cercului, apoi setează raza. Define el centro del círculo, luego establece el radio. 設定圓心位置,再設置圓圈半徑。 15 | LINETOOL FixedLength Fixed length 固定两端 Vaste lengte Fixierte Länge Lungime fixă Longitud fija 固定端點 16 | LINETOOL_DESCRIPTION FixedLength Space items along the full length of the line from start to end, using the set spacing distance only as an approximation. If this is NOT set then items will be spaced exactly according to the set distance, even if it means they can't be placed along the full length of the line. 选择此功能后,物品将以预设间距作为近似值,沿线均匀分布,并且起始与终点固定。若不选择此功能,物品将按照设定的间距精确间隔放置,此时物品无法均匀分布,且终点不固定。 Plaats items langs de volledige lengte van de lijn van begin tot eind, waarbij de ingestelde tussenafstand alleen als een schatting wordt gebruikt. Als dit NIET is ingesteld, worden items precies volgens de ingestelde afstand geplaatst, zelfs als dit betekent dat ze niet over de volledige lengte van de lijn kunnen worden geplaatst. Zwischenräume entlang der gesamten Länge der Linie von Anfang bis Ende verteilen, wobei die festgelegte Abstandsdistanz nur als Annäherungswert dient. Wenn diese Option NICHT gesetzt ist, werden die Elemente genau entsprechend der festgelegten Abstandsdistanz platziert, auch wenn es bedeutet, dass sie nicht entlang der gesamten Länge der Linie platziert werden können. Distanțează elementele pe toată lungimea liniei până la sfârșit, folosind distanța de spațiere setată doar ca o aproximare. Dacă aceasta NU este setată, atunci elementele vor fi spațiate exact în funcție de distanța setată, chiar dacă aceasta înseamnă că nu pot fi plasate pe toată lungimea liniei. Separa a lo largo de la línea desde el inicio hasta el final, utilizando la configuración de separación de manera aproximada. Si no existe un valor, los elementos tendrán una separación acorde a la distancia configurada, aun si esto significa que no se pueden colocar a lo largo de la línea. 啟用此功能時,物品將以預設間距作為近似值,沿線平均分佈,且起點與終點固定。 如不啟用此功能,物品將按照設定的間距精確放置,此時擺放物品將無法平均分佈,終點位置也不固定。 17 | LINETOOL RandomRotation Random rotation 随机旋转 Willekeurige rotatie Zufällige Rotation Rotație aleatorie Rotación aleatoria 隨機旋轉 18 | LINETOOL_DESCRIPTION RandomRotation Rotate each item randomly so they all face in different directions. 随机旋转每个物品,使其分别有不同的朝向。 Draai elk voorwerp willekeurig zodat ze allemaal in verschillende richtingen wijzen. Dreht alle Elemente zufällig, so das sie alle in verschiedene Richtungen ausgerichtet sind. Rotește fiecare element la întâmplare astfel încât toate să fie orientate în direcții diferite. Rota cada elemento de manera aleatoria. 擺放物品時隨機旋轉,使其面向皆不同。 19 | LINETOOL SpacingDown Decrease spacing 减少间距 Verklein afstand Abstand verringern Diminuează spațierea Disminuir separación 減少間距 20 | LINETOOL_DESCRIPTION SpacingDown Change the step size by holding down the shift key (step by 10m) or control key (step by 0.1m) when clicking. 调整参数时,按住 Shift 键 ×10,按住 Ctrl 键 ×0.1。 Wijzig de stapgrootte door de shift-toets ingedrukt te houden (stap van 10m) of de control-toets ingedrukt te houden (stap van 0, m) bij het klikken. Ändere die Schrittgröße durch Drücken der Shift-Taste (Änderung um 10m) oder der STRG-Taste (Änderung um 0,1m) beim Klicken auf die Schaltfläche. Schimbă dimensiunea pasului ținând apăsată tasta Shift (pas de 10m) sau tasta Control (pas de 0.1m) la apăsarea click-ului. Cambia el incremento manteniendo presionada la tecla mayús (incrementa cada 10 m) o la tecla control (incrementa cada 0.1 m) al hacer clic. 調整數值時可使用組合鍵,長按 Shift 鍵時以 10 公尺為單位微調,長按 Ctrl 鍵時以 0.1 公尺為單位微調。 21 | LINETOOL SpacingUp Increase spacing 增加间距 Vergroot afstand Abstand erhöhen Mărește spațierea Incrementar espaciado 增加間距 22 | LINETOOL_DESCRIPTION SpacingUp Change the step size by holding down the shift key (step by 10m) or control key (step by 0.1m) when clicking. 调整参数时,按住 Shift 键 ×10,按住 Ctrl 键 ×0.1。 Wijzig de stapgrootte door de shift-toets ingedrukt te houden (stap van 10m) of de control-toets ingedrukt te houden (stap van 0,1m) bij het klikken. Ändere die Schrittgröße durch Drücken der Shift-Taste (Änderung um 10m) oder der STRG-Taste (Änderung um 0,1m) beim Klicken auf die Schaltfläche. Schimbă dimensiunea pasului ținând apăsată tasta Shift (pas de 10m) sau tasta Control (pas de 0.1m) la apăsarea click-ului. Cambia el incremento manteniendo presionada la tecla mayús (incrementa cada 10 m) o la tecla control (incrementa cada 0.1 m) al hacer clic. 調整數值時可使用組合鍵,長按 Shift 鍵時以 10 公尺為單位微調,長按 Ctrl 鍵時以 0.1 公尺為單位微調。 23 | LINETOOL AntiClockwise Rotate anti-clockwise 逆时针旋转 Draai tegen de klok in Gegen den Uhrzeigersinn drehen Rotește în sens invers acelor de ceasornic Girar hacia la izquierda 逆時針旋轉 24 | LINETOOL_DESCRIPTION AntiClockwise Change the step size by holding down the shift key (rotate by 90°) or control key (rotate by 1°) when clicking. 调整角度时,按住 Shift 键一次可调整90°,按住 Ctrl 键一次可调整1°。 Wijzig de stapgrootte door de shift-toets ingedrukt te houden (roteer met 90°) of de control-toets ingedrukt te houden (roteer met 1°) bij het klikken. Ändere die Schrittgröße durch Drücken der Shift-Taste (Drehung um 90°) oder der STRG-Taste (Drehung um 1°) beim Klicken auf die Schaltfläche. Schimbă dimensiunea pasului ținând apăsată tasta Shift (rotație de 90°) sau tasta Control (rotație de 1°) la apăsarea click-ului. Cambia el incremento manteniendo presionada la tecla mayús (rota 90 °) o la tecla control (rota 1°) al hacer clic. 調整數值時可使用組合鍵,長按 Shift 鍵時以 90° 為單位旋轉,長按 Ctrl 鍵時以 1° 為單位旋轉。 25 | LINETOOL Clockwise Rotate clockwise 顺时针旋转90° Draai met de klok mee Im Uhrzeigersinn drehen Rotește în sensul acelor de ceasornic Girar hacia la derecha 順時針旋轉 26 | LINETOOL_DESCRIPTION Clockwise Change the step size by holding down the shift key (rotate by 90°) or control key (rotate by 1°) when clicking. 调整角度时,按住 Shift 键一次可调整90°,按住 Ctrl 键一次可调整1°。 Wijzig de stapgrootte door de shift-toets ingedrukt te houden (roteer met 90°) of de control-toets ingedrukt te houden (roteer met 1°) bij het klikken. Ändere die Schrittgröße durch Drücken der Shift-Taste (Drehung um 90°) oder der STRG-Taste (Drehung um 1°) beim Klicken auf die Schaltfläche. Schimbă dimensiunea pasului ținând apăsată tasta Shift (rotație de 90°) sau tasta Control (rotație de 1°) la apăsarea click-ului. Cambia el incremento manteniendo presionada la tecla mayús (rota 90°) o la tecla control (rota 1°) al hacer clic. 調整數值時可使用組合鍵,長按 Shift 鍵時以 90° 為單位旋轉,長按 Ctrl 鍵時以 1° 為單位旋轉。 27 | LINETOOL RandomSpacingDown Decrease random spacing variation 减少随机间距变化范围 Verminder de willekeurige variatie in de tussenruimte Verringere die zufällige Abstandsvariation Diminuează variația distanțării aleatorie Disminuir variación aleatorio de espaciado 減少隨機間距的變化範圍 28 | LINETOOL_DESCRIPTION RandomSpacingDown Change the step size by holding down the shift key (step by 10m) or control key (step by 0.1m) when clicking. Set to zero for precise placement. 调整参数时,按住 Shift 键 ×10,按住 Ctrl 键 ×0.1。设置为零以实现精确放置。 Wijzig de stapgrootte door de shift-toets ingedrukt te houden (stap van 10m) of de control-toets ingedrukt te houden (stap van 0,1m) bij het klikken. Zet op nul voor nauwkeurige plaatsing. Ändere die Schrittgröße durch Drücken der Shift-Taste (Änderung um 10m) oder der STRG-Taste (Änderung um 0,1m) beim Klicken auf die Schaltfläche. Auf null setzen, um präzise zu platzieren. Schimbă dimensiunea pasului ținând apăsată tasta Shift (pas de 10m) sau tasta Control (pas de 0.1m) la apăsarea click-ului. Setează zero pentru o plasare precisă. Cambia el incremento manteniendo presionada la tecla mayús (incrementa cada 10 m) o la tecla control (incrementa cada 0.1 m) al hacer clic. Establece el valor a cero para una posición precisa. 調整數值時可使用組合鍵,長按 Shift 鍵時以 10 公尺為單位調整,長按 Ctrl 鍵時以 0.1 公尺為單位調整,將數值設置為零可進行精確放置。 29 | LINETOOL RandomSpacingUp Increase random spacing variation 增加随机间距变化范围 Vergroot de willekeurige variatie in de tussenruimte Erhöhe die zufällige Abstandsvariation Crește variația distanțării aleatorie Incrementar variación de espaciado aleatorio 增加隨機間距的變化範圍 30 | LINETOOL_DESCRIPTION RandomSpacingUp Change the step size by holding down the shift key (step by 10m) or control key (step by 0.1m) when clicking. Set to zero for precise placement. 调整参数时,按住 Shift 键 ×10,按住 Ctrl 键 ×0.1。设置为零以实现精确放置。 Wijzig de stapgrootte door de shift-toets ingedrukt te houden (stap van 10m) of de control-toets ingedrukt te houden (stap van 0,1m) bij het klikken. Zet op nul voor nauwkeurige plaatsing. Ändere die Schrittgröße durch Drücken der Shift-Taste (Änderung um 10m) oder der STRG-Taste (Änderung um 0,1m) beim Klicken auf die Schaltfläche. Auf null setzen, um präzise zu platzieren. Schimbă dimensiunea pasului ținând apăsată tasta Shift (pas de 10m) sau tasta Control (pas de 0.1m) la apăsarea click-ului. Setează zero pentru o plasare precisă. Cambia el incremento manteniendo presionada la tecla mayús (incrementa cada 10 m) o la tecla control (incrementa cada 0.1 m) al hacer clic. Establece el valor a cero para una posición precisa. 調整數值時可使用組合鍵,長按 Shift 鍵時以 10 公尺為單位調整,長按 Ctrl 鍵時以 0.1 公尺為單位調整,將數值設置為零可進行精確放置。 31 | LINETOOL RandomOffsetDown Decrease random sideways variation 减少两侧偏移范围 Verminder de willekeurige zijdelingse variatie Verringere die zufällige seitliche Variation Diminuează variația laterală aleatorie Disminuir variación aleatoria de los lados 減少兩側偏移範圍 32 | LINETOOL_DESCRIPTION RandomOffsetDown Change the step size by holding down the shift key (step by 10m) or control key (step by 0.1m) when clicking. Set to zero for precise placement. 调整参数时,按住 Shift 键 ×10,按住 Ctrl 键 ×0.1。设置为零以实现精确放置。 Wijzig de stapgrootte door de shift-toets ingedrukt te houden (stap van 10m) of de control-toets ingedrukt te houden (stap van 0,1m) bij het klikken. Zet op nul voor nauwkeurige plaatsing. Ändere die Schrittgröße durch Drücken der Shift-Taste (Änderung um 10m) oder der STRG-Taste (Änderung um 0,1m) beim Klicken auf die Schaltfläche. Auf null setzen, um präzise zu platzieren. Schimbă dimensiunea pasului ținând apăsată tasta Shift (pas de 10m) sau tasta Control (pas de 0.1m) la apăsarea click-ului. Setează zero pentru o plasare precisă. Cambia el incremento manteniendo presionada la tecla mayús (incrementa cada 10 m) o la tecla control (incrementa cada 0.1 m) al hacer clic. Establece el valor a cero para una posición precisa. 調整數值時可使用組合鍵,長按 Shift 鍵時以 10 公尺為單位調整,長按 Ctrl 鍵時以 0.1 公尺為單位調整,將數值設置為零可進行精確放置。 33 | LINETOOL RandomOffsetUp Increase random sideways variation 增加两侧偏移范围 Vergroot de willekeurige zijdelingse variatie Erhöhe die zufällige seitliche Variation Crește variația laterală aleatorie Incrementar variación aleatoria de los lados 增加兩側偏移範圍 34 | LINETOOL_DESCRIPTION RandomOffsetUp Change the step size by holding down the shift key (step by 10m) or control key (step by 0.1m) when clicking. Set to zero for precise placement. 调整参数时,按住 Shift 键 ×10,按住 Ctrl 键 ×0.1。设置为零以实现精确放置。 Wijzig de stapgrootte door de shift-toets ingedrukt te houden (stap van 10m) of de control-toets ingedrukt te houden (stap van 0,1m) bij het klikken. Zet op nul voor nauwkeurige plaatsing. Ändere die Schrittgröße durch Drücken der Shift-Taste (Änderung um 10m) oder der STRG-Taste (Änderung um 0,1m) beim Klicken auf die Schaltfläche. Auf null setzen, um präzise zu platzieren. Schimbă dimensiunea pasului ținând apăsată tasta Shift (pas de 10m) sau tasta Control (pas de 0.1m) la apăsarea click-ului. Setează zero pentru o plasare precisă. Cambia el incremento manteniendo presionada la tecla mayús (incrementa cada 10 m) o la tecla control (incrementa cada 0.1 m) al hacer clic. Establece el valor a cero para una posición precisa. 調整數值時可使用組合鍵,長按 Shift 鍵時以 10 公尺為單位調整,長按 Ctrl 鍵時以 0.1 公尺為單位調整,將數值設置為零可進行精確放置。 35 | LINETOOL Spacing Spacing 间距 Afstand Abstand Spațiere Espaciado 間距 36 | LINETOOL_DESCRIPTION Spacing Objects will be spaced apart by this amount (or approximately this amount if fixed length spacing is selected). 物品将按照此间距间隔放置(如果选择了固定两端功能,那么间距会尽可能的接近您设置的数值)。 Objecten worden gescheiden door dit bedrag (of ongeveer dit bedrag als vaste lengteafstand is geselecteerd). Objekte werden durch diesen Abstandswert voneinander getrennt (oder ungefähr diesen Wert, wenn die fixierte Länge ausgewählt wird). Obiectele vor fi distanțate cu această valoare (sau aproximativ cu această valoare dacă distanța fixă de spațiere este selectată). Los elementos serán separados por esta cantidad (o aproximadamente esta cantidad si se selecciona un espaciado de longitud fija). 設定物品擺放時間隔的固定距離(若啟用固定端點模式,間距則為設定數值的近似值) 37 | LINETOOL Rotation Rotation 旋转 Rotatie Drehung Rotație Rotación 旋轉 38 | LINETOOL_DESCRIPTION Rotation Objects will be rotated by this amount. 物品将按照此角度旋转。 Objecten zullen met deze hoeveelheid worden geroteerd. Objekte werden um diesen Wert gedreht. Obiectele vor fi rotite cu această valoare. Los elementos se rotarán por esta cantidad. 物品將依照此角度旋轉。 39 | LINETOOL SpacingVariation Spacing variation 间距变化范围 Variatie in tussenruimte Abstandsvariation Variația spațierii Variación de espaciado 間距變化範圍 40 | LINETOOL_DESCRIPTION SpacingVariation Object spacing along the line will be varied by a random distance up to this maximum. Set to zero for precise placement. 沿线物品的间距将按照此数值随机变化。设置为零以实现精确放置。 De tussenruimte van objecten langs de lijn zal variëren met een willekeurige afstand tot dit maximum. Zet op nul voor nauwkeurige plaatsing. Objektabstand entlang der Linie wird bis zu diesem Maximum um eine zufällige Distanz verändert. Auf null setzen für genaue Platzierung. Spațierea obiectelor de-a lungul liniei va fi modificată cu o distanță aleatorie până la acest maxim. Setează zero pentru o plasare precisă. El espaciado de elementos a lo largo de la línea será variado por una distancia aleatoria hasta este máximo. Establece el valor a cero para una posición precisa. 路徑上物品的間距將依照此數值隨機變化,設置為零時可進行精確放置。 41 | LINETOOL OffsetVariation Offset variation 偏移范围 Variatie in verschuiving Versatzvariation Variația decalajului Variación de desplazamiento 偏移範圍 42 | LINETOOL_DESCRIPTION OffsetVariation Objects will be randomly offset sideways from the line up to this maximum distance. Set to zero for precise placement. 物品将从直线向两侧随机偏移。设置为零以实现精确放置。 Objecten zullen willekeurig zijdelings worden verschoven vanaf de lijn tot aan deze maximale afstand. Zet op nul voor nauwkeurige plaatsing. Objekte werden zufällig seitlich von der Linie bis zu dieser maximalen Distanz versetzt. Auf null setzen für genaue Platzierung. Obiectele vor fi decalate aleatoriu în lateral față de linie până la această distanță maximă. Setează zero pentru o plasare precisă. Los elementos serán desplazados aleatoriamente desde la línea hasta esta distancia máxima. Establece el valor a cero para una posición precisa. 物品將在直線上向兩側隨機偏移,設置為零時可進行精確放置。 43 | -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "companyName": "algernon (K. Algernon A. Sheppard)", 6 | "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} (the \"License\"); you may not use this file except in compliance with the License.\nSee {licenseFile} file in the project root for full license information.", 7 | "variables": { 8 | "licenseName": "Apache Licence, Version 2.0", 9 | "licenseFile": "LICENSE.txt" 10 | } 11 | } 12 | } 13 | } 14 | --------------------------------------------------------------------------------