├── .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 |
--------------------------------------------------------------------------------
/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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
0 m
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
0°
62 |
63 |
64 |
65 |
66 |
67 |
68 |
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 |
--------------------------------------------------------------------------------