From 5653e024e09b2024dec53901385ae73bab566379 Mon Sep 17 00:00:00 2001 From: algernon-A Date: Sat, 2 Dec 2023 16:02:03 +1100 Subject: [PATCH] 1.0.3 - add initial fence mode. --- BepInEx/manifest.json | 2 +- Changelog.txt | 3 ++ Code/LineModes/Circle.cs | 7 ++-- Code/LineModes/LineBase.cs | 21 ++++++++---- Code/LineModes/SimpleCurve.cs | 24 ++++++++----- Code/Systems/LineToolSystem.cs | 52 ++++++++++++++++++++++++---- Code/Systems/LineToolUISystem.cs | 20 +++++++++-- Icons/Fence.svg | 16 +++++++++ LineToolLite.csproj | 2 +- README.md | 5 ++- UI/ui.html | 3 ++ UI/ui.js | 58 +++++++++++++++++++++++++++++--- 12 files changed, 180 insertions(+), 33 deletions(-) create mode 100644 Icons/Fence.svg diff --git a/BepInEx/manifest.json b/BepInEx/manifest.json index 64e239d..f47f7f5 100644 --- a/BepInEx/manifest.json +++ b/BepInEx/manifest.json @@ -1,6 +1,6 @@ { "name": "Line_Tool_Lite", - "version_number": "1.0.2", + "version_number": "1.0.3", "website_url": "https://github.com/algernon-A/LineToolLite", "description": "Place objects in lines, curves, or circles", "dependencies": [ diff --git a/Changelog.txt b/Changelog.txt index b3bdd19..65c7a1c 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,6 @@ +1.0.3 +- Add initial fence mode. + 1.0.2 - Rework UI JavaScript to survive UI regeneration and enable compatibility with HookUI (thanks to Captain of Coit). - Remove JavaScript globals. diff --git a/Code/LineModes/Circle.cs b/Code/LineModes/Circle.cs index 6fa21de..90db761 100644 --- a/Code/LineModes/Circle.cs +++ b/Code/LineModes/Circle.cs @@ -4,6 +4,7 @@ namespace LineTool { + using Colossal.Mathematics; using Game.Simulation; using Unity.Collections; using Unity.Mathematics; @@ -26,11 +27,13 @@ public Circle(LineBase mode) /// Calculates the points to use based on this mode. /// /// Selection current position. + /// Set to true if fence mode is active. /// Spacing setting. /// Rotation setting. + /// Prefab zBounds. /// List of points to populate. /// Terrain height data reference. - public override void CalculatePoints(float3 currentPos, float spacing, int rotation, NativeList pointList, ref TerrainHeightData heightData) + public override void CalculatePoints(float3 currentPos, bool fenceMode, float spacing, int rotation, Bounds1 zBounds, NativeList pointList, ref TerrainHeightData heightData) { // Don't do anything if we don't have valid start. if (!m_validStart) @@ -59,7 +62,7 @@ public override void CalculatePoints(float3 currentPos, float spacing, int rotat thisPoint.y = TerrainUtils.SampleHeight(ref heightData, thisPoint); // Add point to list. - pointList.Add(new PointData { Position = thisPoint, Rotation = quaternion.Euler(0f, math.radians(i + rotation), 0f), }); + pointList.Add(new PointData { Position = thisPoint, Rotation = quaternion.Euler(0f, math.radians(rotation) - i, 0f), }); } } diff --git a/Code/LineModes/LineBase.cs b/Code/LineModes/LineBase.cs index 372c1e8..c5cb660 100644 --- a/Code/LineModes/LineBase.cs +++ b/Code/LineModes/LineBase.cs @@ -92,11 +92,13 @@ public virtual void ItemsPlaced(float3 position) /// Calculates the points to use based on this mode. /// /// Selection current position. + /// Set to true if fence mode is active. /// Spacing setting. /// Rotation setting. + /// Prefab zBounds. /// List of points to populate. /// Terrain height data reference. - public virtual void CalculatePoints(float3 currentPos, float spacing, int rotation, NativeList pointList, ref TerrainHeightData heightData) + public virtual void CalculatePoints(float3 currentPos, bool fenceMode, float spacing, int rotation, Bounds1 zBounds, NativeList pointList, ref TerrainHeightData heightData) { // Don't do anything if we don't have a valid start point. if (!m_validStart) @@ -107,17 +109,24 @@ public virtual void CalculatePoints(float3 currentPos, float spacing, int rotati // Calculate length. float length = math.length(currentPos - m_startPos); + // Calculate applied rotation (in radians). + float appliedRotation = math.radians(rotation); + if (fenceMode) + { + float3 difference = currentPos - m_startPos; + appliedRotation = math.atan2(difference.x, difference.z); + } + // Rotation quaternion. - quaternion rotationQuaternion = quaternion.Euler(0f, math.radians(rotation), 0f); + quaternion rotationQuaternion = quaternion.Euler(0f, appliedRotation, 0f); // Create points. - float currentDistance = 0f; - while (currentDistance < length) + float currentDistance = fenceMode ? -zBounds.min : 0f; + float endLength = fenceMode ? length - zBounds.max : length; + while (currentDistance < endLength) { // Calculate interpolated point. float3 thisPoint = math.lerp(m_startPos, currentPos, currentDistance / length); - - // Calculate terrain height. thisPoint.y = TerrainUtils.SampleHeight(ref heightData, thisPoint); // Add point to list. diff --git a/Code/LineModes/SimpleCurve.cs b/Code/LineModes/SimpleCurve.cs index cee3597..fd4f6fb 100644 --- a/Code/LineModes/SimpleCurve.cs +++ b/Code/LineModes/SimpleCurve.cs @@ -81,11 +81,13 @@ public override void ItemsPlaced(float3 position) /// Calculates the points to use based on this mode. /// /// Selection current position. + /// Set to true if fence mode is active. /// Spacing setting. /// Rotation setting. + /// Prefab zBounds. /// List of points to populate. /// Terrain height data reference. - public override void CalculatePoints(float3 currentPos, float spacing, int rotation, NativeList pointList, ref TerrainHeightData heightData) + public override void CalculatePoints(float3 currentPos, bool fenceMode, float spacing, int rotation, Bounds1 zBounds, NativeList pointList, ref TerrainHeightData heightData) { // Don't do anything if we don't have valid start. if (!m_validStart) @@ -96,31 +98,37 @@ public override void CalculatePoints(float3 currentPos, float spacing, int rotat // If we have a valid start but no valid elbow, just draw a straight line. if (!m_validElbow) { - base.CalculatePoints(currentPos, spacing, rotation, pointList, ref heightData); + base.CalculatePoints(currentPos, fenceMode, spacing, rotation, zBounds, pointList, ref heightData); return; } // Calculate Bezier. _thisBezier = NetUtils.FitCurve(new Line3.Segment(m_startPos, m_elbowPoint), new Line3.Segment(currentPos, m_elbowPoint)); - // Rotation quaternion. + // Default rotation quaternion. quaternion qRotation = quaternion.Euler(0f, math.radians(rotation), 0f); - // Create points. float tFactor = 0f; while (tFactor < 1.0f) { // Calculate point. - Vector3 thisPoint = MathUtils.Position(_thisBezier, tFactor); + float3 thisPoint = MathUtils.Position(_thisBezier, tFactor); + + // Get next t factor. + tFactor = BezierStep(tFactor, spacing); + + // Calculate applied rotation for fence mode. + if (fenceMode) + { + float3 difference = MathUtils.Position(_thisBezier, tFactor) - thisPoint; + qRotation = quaternion.Euler(0f, math.atan2(difference.x, difference.z), 0f); + } // Calculate terrain height. thisPoint.y = TerrainUtils.SampleHeight(ref heightData, thisPoint); // Add point to list. pointList.Add(new PointData { Position = thisPoint, Rotation = qRotation, }); - - // Get next t factor. - tFactor = BezierStep(tFactor, spacing); } } diff --git a/Code/Systems/LineToolSystem.cs b/Code/Systems/LineToolSystem.cs index 0179df6..4ef3246 100644 --- a/Code/Systems/LineToolSystem.cs +++ b/Code/Systems/LineToolSystem.cs @@ -7,6 +7,7 @@ namespace LineTool using System.Reflection; using Colossal.Entities; using Colossal.Logging; + using Colossal.Mathematics; using Game.Common; using Game.Input; using Game.Objects; @@ -35,6 +36,7 @@ public sealed partial class LineToolSystem : ObjectToolBaseSystem // Line calculations. private readonly NativeList _points = new (Allocator.Persistent); + private bool _fenceMode = false; private bool _fixedPreview = false; private float3 _fixedPos; private Random _random = new (); @@ -45,9 +47,10 @@ public sealed partial class LineToolSystem : ObjectToolBaseSystem private Entity _cursorEntity = Entity.Null; // Prefab selection. - private ObjectPrefab _selectedPrefab; + private ObjectGeometryPrefab _selectedPrefab; private Entity _selectedEntity = Entity.Null; private int _originalXP; + private Bounds1 _zBounds; // References. private ILog _log; @@ -81,11 +84,21 @@ public sealed partial class LineToolSystem : ObjectToolBaseSystem public override string toolID => "Line Tool"; /// - /// Gets or sets the line spacing. + /// Gets or sets the effective line spacing. /// internal float Spacing { - get => _spacing; + get + { + // Use calculated spacing for fence mode. + if (_fenceMode) + { + return _zBounds.max - _zBounds.min; + } + + // Not fence mode - use manual spacing. + return _spacing; + } set { @@ -94,6 +107,19 @@ internal float Spacing } } + /// + /// Gets or sets a value indicating whether fence mode is active. + /// + internal bool FenceMode + { + get => _fenceMode; + set + { + _fenceMode = value; + _dirty = true; + } + } + /// /// Gets or sets a value indicating whether random rotation is active. /// @@ -171,11 +197,12 @@ private PrefabBase SelectedPrefab { set { - _selectedPrefab = value as ObjectPrefab; + _selectedPrefab = value as ObjectGeometryPrefab; - // Update selected entity; + // Update selected entity. if (_selectedPrefab is null) { + // No valid entity selected. _selectedEntity = Entity.Null; } else @@ -183,6 +210,19 @@ private PrefabBase SelectedPrefab // Get selected entity. _selectedEntity = m_PrefabSystem.GetEntity(_selectedPrefab); + // Check bounds. + _zBounds.min = 0; + _zBounds.max = 0; + foreach (ObjectMeshInfo mesh in _selectedPrefab.m_Meshes) + { + if (mesh.m_Mesh is RenderPrefab renderPrefab) + { + // Update bounds if either of the z extents of this mesh exceed the previous extent. + _zBounds.min = math.min(_zBounds.min, renderPrefab.bounds.z.min); + _zBounds.max = math.max(_zBounds.max, renderPrefab.bounds.z.max); + } + } + // Reduce any XP to zero while we're using the tool. if (EntityManager.TryGetComponent(_selectedEntity, out PlaceableObjectData placeableData)) { @@ -440,7 +480,7 @@ protected override JobHandle OnUpdate(JobHandle inputDeps) // If we got here we're (re)calculating points. _points.Clear(); - _mode.CalculatePoints(position, Spacing, _rotation, _points, ref _terrainHeightData); + _mode.CalculatePoints(position, _fenceMode, Spacing, _rotation, _zBounds, _points, ref _terrainHeightData); // Step along length and place objects. int count = 0; diff --git a/Code/Systems/LineToolUISystem.cs b/Code/Systems/LineToolUISystem.cs index 24cce2d..fc3d039 100644 --- a/Code/Systems/LineToolUISystem.cs +++ b/Code/Systems/LineToolUISystem.cs @@ -105,8 +105,17 @@ protected override void OnUpdate() { ExecuteScript(_uiView, $"document.getElementById(\"line-tool-rotation-random\").classList.add(\"selected\");"); - // Hide rotation button. - ExecuteScript(_uiView, "lineToolSetRotationVisibility(false);"); + // Hide rotation buttons. + ExecuteScript(_uiView, "lineTool.setRotationVisibility(false);"); + } + + // Select fence mode button if needed. + if (_lineToolSystem.FenceMode) + { + // Hide rotation and spacing buttons. + ExecuteScript(_uiView, $"document.getElementById(\"line-tool-fence\").classList.add(\"selected\");"); + ExecuteScript(_uiView, "let randomRotationButton = document.getElementById(\"line-tool-rotation-random\"); lineTool.setButtonVisibility(randomRotationButton, false);"); + ExecuteScript(_uiView, "lineTool.setRotationVisibility(false); lineTool.setSpacingVisibility(false);"); } // Show tree control menu if tree control is active. @@ -117,6 +126,7 @@ protected override void OnUpdate() // Register event callbacks. _eventHandles.Add(_uiView.RegisterForEvent("SetLineToolSpacing", (Action)SetSpacing)); + _eventHandles.Add(_uiView.RegisterForEvent("SetLineToolFenceMode", (Action)SetFenceMode)); _eventHandles.Add(_uiView.RegisterForEvent("SetLineToolRandomRotation", (Action)SetRandomRotation)); _eventHandles.Add(_uiView.RegisterForEvent("SetLineToolRotation", (Action)SetRotation)); _eventHandles.Add(_uiView.RegisterForEvent("SetStraightMode", (Action)SetStraightMode)); @@ -316,6 +326,12 @@ private string EscapeToJavaScript(string sourceString) /// Value to set. private void SetSpacing(float spacing) => _lineToolSystem.Spacing = spacing; + /// + /// Event callback to set fence mode. + /// + /// Value to set. + private void SetFenceMode(bool isActive) => _lineToolSystem.FenceMode = isActive; + /// /// Event callback to set the random rotation override. /// diff --git a/Icons/Fence.svg b/Icons/Fence.svg new file mode 100644 index 0000000..f443f94 --- /dev/null +++ b/Icons/Fence.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/LineToolLite.csproj b/LineToolLite.csproj index befd706..31faaf0 100644 --- a/LineToolLite.csproj +++ b/LineToolLite.csproj @@ -6,7 +6,7 @@ algernon Copyright © 2023 algernon (github.com/algernon-A). All rights reserved. $(Title) - 1.0.2 + 1.0.3 9.0 True diff --git a/README.md b/README.md index 9c3c071..253ff9d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Cities Skylines 2 : Line Tool Lite - **Place objects in lines** - straight lines, curves, or circles. +- **Fence mode** - automatically align objects end-to-end. - **Accurate placement** - no worrying about imprecision. - Easily adjust **spacing** and **rotation** using the in-game tool UI. - Live **previewing** included, so you can provisionally place a line and adjust spacing and/or rotation to see how it looks in real-time before making the final placement (or cancelling). @@ -19,6 +20,7 @@ Works on all types of objects - trees, shrubs, props. You can even place simple - **Control-click** at the end will leave the line in preview mode; it's not fully placed yet so you can go and adjust the settings and see the results in real time. When finished, **left-click** to place or **right-click** to cancel. ### Use the tool UI to: +- Toggle **fence mode**. - Toggle between **straight line**, **curved**, and **circle** modes. - Adjust **distances** using the arrow buttons - plain click for 1m increments, **shift-click** for 10m, **control-click** for 0.1m. - Select **random rotation** to have each object in the line have a different randomly-chosen rotation, or otherwise **manually adjust the rotation** for all items using the arrow buttons - plain click for 10-degree increments, **shift-click** for 90 degrees, **control-click** for 1 degree. @@ -30,9 +32,6 @@ Works on all types of objects - trees, shrubs, props. You can even place simple ## Requirements - BepInEx 5 -## Conflicts -- HookUI - ## Installation 1. Make sure that BepInEx 5 is installed. 1. Make sure that you're **NOT** using HookUI or mods that use it (such as Unemployment Monitor, Extended Tooltip, City Monitor, or Vehicle Counter). diff --git a/UI/ui.html b/UI/ui.html index f36c2e5..5508b02 100644 --- a/UI/ui.html +++ b/UI/ui.html @@ -3,6 +3,9 @@
Line mode
+ diff --git a/UI/ui.js b/UI/ui.js index d73fc55..386e1a5 100644 --- a/UI/ui.js +++ b/UI/ui.js @@ -53,19 +53,54 @@ if (typeof lineTool.adjustRotation !== 'function') { } } +// Function to implement fence mode selection. +if (typeof lineTool.fenceMode !== 'function') { + lineTool.fenceMode = function () { + var fenceModeButton = document.getElementById("line-tool-fence"); + if (fenceModeButton.classList.contains("selected")) { + fenceModeButton.classList.remove("selected"); + engine.trigger('SetLineToolFenceMode', false); + + // Show spacing and random rotation button. + lineTool.setSpacingVisibility(true); + let randomRotationButton = document.getElementById("line-tool-rotation-random"); + lineTool.setButtonVisibility(randomRotationButton, true); + + // Show rotation, but only if random rotation is not set. + if (!randomRotationButton.classList.contains("selected")) { + lineTool.setRotationVisibility(true); + } + } + else { + fenceModeButton.classList.add("selected"); + engine.trigger('SetLineToolFenceMode', true); + + // Disable random rotation and hide button. + let randomRotationButton = document.getElementById("line-tool-rotation-random"); + randomRotationButton.classList.remove("selected"); + engine.trigger('SetLineToolRandomRotation', false); + lineTool.setButtonVisibility(randomRotationButton, false); + + // Hide rotation tools. + lineTool.setSpacingVisibility(false); + lineTool.setRotationVisibility(false); + } + } +} + // Function to implement random rotation selection. if (typeof lineTool.randomRotation !== 'function') { lineTool.randomRotation = function() { - var adjustRotationButton = document.getElementById("line-tool-rotation-random"); - if (adjustRotationButton.classList.contains("selected")) { - adjustRotationButton.classList.remove("selected"); + var randomRotationButton = document.getElementById("line-tool-rotation-random"); + if (randomRotationButton.classList.contains("selected")) { + randomRotationButton.classList.remove("selected"); engine.trigger('SetLineToolRandomRotation', false); // Show rotation tools. lineTool.setRotationVisibility(true); } else { - adjustRotationButton.classList.add("selected"); + randomRotationButton.classList.add("selected"); engine.trigger('SetLineToolRandomRotation', true); // Hide rotation tools. @@ -115,6 +150,20 @@ if (typeof lineTool.handleCircleMode !== 'function') { } } +// Function to set spacing selection control visibility +if (typeof lineTool.setSpacingVisibility !== 'function') { + lineTool.setSpacingVisibility = function (isVisible) { + lineTool.setButtonVisibility(document.getElementById("line-tool-spacing-up"), isVisible); + lineTool.setButtonVisibility(document.getElementById("line-tool-spacing-down"), isVisible); + if (isVisible) { + document.getElementById("line-tool-spacing-field").style.visibility = "visible"; + } + else { + document.getElementById("line-tool-spacing-field").style.visibility = "hidden"; + } + } +} + // Function to set rotation selection control visibility if (typeof lineTool.setRotationVisibility !== 'function') { lineTool.setRotationVisibility = function(isVisible) { @@ -152,6 +201,7 @@ lineTool.adjustSpacing(null, 0); lineTool.adjustRotation(null, 0); // Add button event handlers. +document.getElementById("line-tool-fence").onmousedown = () => { lineTool.fenceMode(); } document.getElementById("line-tool-spacing-down").onmousedown = (event) => { lineTool.adjustSpacing(event, -1); } document.getElementById("line-tool-spacing-up").onmousedown = (event) => { lineTool.adjustSpacing(event, 1); }