mirror of
https://github.com/appen-isen/jeu-sans-image.git
synced 2026-01-18 16:47:37 +01:00
Code cleaning of SvgToFlatMeshEditor
This commit is contained in:
@@ -9,15 +9,14 @@ using UnityEngine;
|
|||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using Unity.VectorGraphics; // From com.unity.vectorgraphics
|
using Unity.VectorGraphics; // From com.unity.vectorgraphics
|
||||||
|
|
||||||
public class SvgToFlatMeshEditor : EditorWindow
|
public class SvgToFlatMeshEditor : EditorWindow
|
||||||
{
|
{
|
||||||
TextAsset svgFile;
|
TextAsset svgFile;
|
||||||
Material editorMaterial;
|
Material editorMaterial;
|
||||||
float pixelsPerUnit = 100.0f;
|
float pixelsPerUnit = 100.0f;
|
||||||
Transform parentTransform;
|
Transform parentTransform;
|
||||||
float meshScale = 1f;
|
float meshScale = 1f;
|
||||||
VectorUtils.TessellationOptions tessOptions = new VectorUtils.TessellationOptions()
|
VectorUtils.TessellationOptions tessOptions = new VectorUtils.TessellationOptions() {
|
||||||
{
|
|
||||||
StepDistance = 1.0f,
|
StepDistance = 1.0f,
|
||||||
MaxCordDeviation = 0.5f,
|
MaxCordDeviation = 0.5f,
|
||||||
MaxTanAngleDeviation = 0.1f,
|
MaxTanAngleDeviation = 0.1f,
|
||||||
@@ -25,14 +24,12 @@ public class SvgToFlatMeshEditor : EditorWindow
|
|||||||
};
|
};
|
||||||
|
|
||||||
[MenuItem("Tools/SVG → Flat Mesh Regions")]
|
[MenuItem("Tools/SVG → Flat Mesh Regions")]
|
||||||
static void OpenWindow()
|
static void OpenWindow() {
|
||||||
{
|
|
||||||
var w = GetWindow<SvgToFlatMeshEditor>("SVG → Flat Mesh");
|
var w = GetWindow<SvgToFlatMeshEditor>("SVG → Flat Mesh");
|
||||||
w.minSize = new Vector2(460, 320);
|
w.minSize = new Vector2(460, 320);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnGUI()
|
void OnGUI() {
|
||||||
{
|
|
||||||
EditorGUILayout.LabelField("SVG → Flat Mesh (separate GameObjects per fill color)", EditorStyles.boldLabel);
|
EditorGUILayout.LabelField("SVG → Flat Mesh (separate GameObjects per fill color)", EditorStyles.boldLabel);
|
||||||
EditorGUILayout.Space();
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
@@ -44,27 +41,22 @@ public class SvgToFlatMeshEditor : EditorWindow
|
|||||||
|
|
||||||
EditorGUILayout.Space();
|
EditorGUILayout.Space();
|
||||||
EditorGUILayout.LabelField("Tessellation Options", EditorStyles.boldLabel);
|
EditorGUILayout.LabelField("Tessellation Options", EditorStyles.boldLabel);
|
||||||
tessOptions.StepDistance = EditorGUILayout.FloatField("Step Distance", tessOptions.StepDistance);
|
tessOptions.StepDistance = EditorGUILayout.FloatField(new GUIContent("Step Distance", "From manual: The uniform tessellation step distance."), tessOptions.StepDistance);
|
||||||
tessOptions.MaxCordDeviation = EditorGUILayout.FloatField("Max Cord Deviation", tessOptions.MaxCordDeviation);
|
tessOptions.MaxCordDeviation = EditorGUILayout.FloatField(new GUIContent("Max Cord Deviation", "From manual: The maximum distance on the cord to a straight line between to points after which more tessellation will be generated"), tessOptions.MaxCordDeviation);
|
||||||
tessOptions.MaxTanAngleDeviation = EditorGUILayout.FloatField("Max Tan Angle Deviation", tessOptions.MaxTanAngleDeviation);
|
tessOptions.MaxTanAngleDeviation = EditorGUILayout.FloatField(new GUIContent("Max Tan Angle Deviation", "From manual: The maximum angle (in degrees) between the curve tangent and the next point after which more tessellation will be generated"), tessOptions.MaxTanAngleDeviation);
|
||||||
tessOptions.SamplingStepSize = EditorGUILayout.FloatField("Sampling Step Size", tessOptions.SamplingStepSize);
|
tessOptions.SamplingStepSize = EditorGUILayout.FloatField(new GUIContent("Sampling Step Size", "From manual: The number of samples used internally to evaluate the curves. More samples = higher quality. Should be between 0 and 1 (inclusive)"), tessOptions.SamplingStepSize);
|
||||||
|
|
||||||
EditorGUILayout.Space();
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
if (GUILayout.Button("Generate Meshes from SVG"))
|
if (GUILayout.Button("Generate Meshes from SVG")) {
|
||||||
{
|
if (svgFile == null) {
|
||||||
if (svgFile == null)
|
|
||||||
{
|
|
||||||
EditorUtility.DisplayDialog("Error", "Please assign an SVG (.svg) TextAsset.", "OK");
|
EditorUtility.DisplayDialog("Error", "Please assign an SVG (.svg) TextAsset.", "OK");
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
{
|
try {
|
||||||
try
|
|
||||||
{
|
|
||||||
GenerateMeshesFromSVG();
|
GenerateMeshesFromSVG();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e) {
|
||||||
{
|
|
||||||
Debug.LogException(e);
|
Debug.LogException(e);
|
||||||
EditorUtility.DisplayDialog("Error", "Exception: " + e.Message, "OK");
|
EditorUtility.DisplayDialog("Error", "Exception: " + e.Message, "OK");
|
||||||
}
|
}
|
||||||
@@ -72,22 +64,19 @@ public class SvgToFlatMeshEditor : EditorWindow
|
|||||||
}
|
}
|
||||||
|
|
||||||
EditorGUILayout.Space();
|
EditorGUILayout.Space();
|
||||||
EditorGUILayout.HelpBox("This tool creates one GameObject per fill-color region in the SVG. Each GameObject receives a MeshRenderer + MeshFilter and, optionally, a MeshCollider. Output meshes lie flat on the XZ plane (Y = 0).", MessageType.Info);
|
EditorGUILayout.HelpBox("This tool creates one GameObject per fill-color region in the SVG. Each GameObject receives a MeshRenderer + MeshFilter and a MeshCollider. Output meshes lie flat on the XZ plane (Y = 0).", MessageType.Info);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenerateMeshesFromSVG()
|
void GenerateMeshesFromSVG() {
|
||||||
{
|
|
||||||
// Parse SVG
|
// Parse SVG
|
||||||
var svgText = svgFile.text;
|
var svgText = svgFile.text;
|
||||||
if (string.IsNullOrEmpty(svgText))
|
if (string.IsNullOrEmpty(svgText)) {
|
||||||
{
|
|
||||||
EditorUtility.DisplayDialog("Error", "SVG file is empty.", "OK");
|
EditorUtility.DisplayDialog("Error", "SVG file is empty.", "OK");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sceneInfo = SVGParser.ImportSVG(new StringReader(svgText));
|
var sceneInfo = SVGParser.ImportSVG(new StringReader(svgText));
|
||||||
if (sceneInfo.Equals(default(SVGParser.SceneInfo)) || sceneInfo.Scene == null)
|
if (sceneInfo.Equals(default(SVGParser.SceneInfo)) || sceneInfo.Scene == null) {
|
||||||
{
|
|
||||||
EditorUtility.DisplayDialog("Error", "Failed to import SVG. Make sure the file is valid and Vector Graphics package is installed.", "OK");
|
EditorUtility.DisplayDialog("Error", "Failed to import SVG. Make sure the file is valid and Vector Graphics package is installed.", "OK");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -99,19 +88,19 @@ public class SvgToFlatMeshEditor : EditorWindow
|
|||||||
|
|
||||||
TraverseAndCollectShapes(sceneInfo.Scene.Root, Matrix2D.identity, shapesByColor);
|
TraverseAndCollectShapes(sceneInfo.Scene.Root, Matrix2D.identity, shapesByColor);
|
||||||
|
|
||||||
if (shapesByColor.Count == 0)
|
if (shapesByColor.Count == 0) {
|
||||||
{
|
|
||||||
EditorUtility.DisplayDialog("Result", "No filled shapes found in the SVG.", "OK");
|
EditorUtility.DisplayDialog("Result", "No filled shapes found in the SVG.", "OK");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create parent container
|
// Create parent container
|
||||||
GameObject container = new GameObject(Path.GetFileNameWithoutExtension(svgFile.name) + "_SVG_Meshes");
|
GameObject container = new GameObject(Path.GetFileNameWithoutExtension(svgFile.name) + "_SVG_Meshes");
|
||||||
if (parentTransform != null) container.transform.SetParent(parentTransform, false);
|
if (parentTransform != null) {
|
||||||
|
container.transform.SetParent(parentTransform, false);
|
||||||
|
}
|
||||||
|
|
||||||
// For each color group, tessellate shapes into geometry and build a mesh
|
// For each color group, tessellate shapes into geometry and build a mesh
|
||||||
foreach (var kv in shapesByColor)
|
foreach (var kv in shapesByColor) {
|
||||||
{
|
|
||||||
Color color = kv.Key;
|
Color color = kv.Key;
|
||||||
List<SceneNodeShapeEntry> entries = kv.Value;
|
List<SceneNodeShapeEntry> entries = kv.Value;
|
||||||
|
|
||||||
@@ -120,11 +109,9 @@ public class SvgToFlatMeshEditor : EditorWindow
|
|||||||
tmpScene.Root = new SceneNode();
|
tmpScene.Root = new SceneNode();
|
||||||
tmpScene.Root.Children = new List<SceneNode>();
|
tmpScene.Root.Children = new List<SceneNode>();
|
||||||
|
|
||||||
foreach (var entry in entries)
|
foreach (var entry in entries) {
|
||||||
{
|
|
||||||
// create a shallow copy Node with transform and the original shapes (the shape objects can be reused)
|
// create a shallow copy Node with transform and the original shapes (the shape objects can be reused)
|
||||||
SceneNode copyNode = new SceneNode()
|
SceneNode copyNode = new SceneNode() {
|
||||||
{
|
|
||||||
Transform = entry.Node.Transform, // keep transform
|
Transform = entry.Node.Transform, // keep transform
|
||||||
Shapes = new List<Shape>() { entry.Shape }
|
Shapes = new List<Shape>() { entry.Shape }
|
||||||
};
|
};
|
||||||
@@ -134,8 +121,7 @@ public class SvgToFlatMeshEditor : EditorWindow
|
|||||||
// Tessellate the tmpScene
|
// Tessellate the tmpScene
|
||||||
var geoms = VectorUtils.TessellateScene(tmpScene, tessOptions);
|
var geoms = VectorUtils.TessellateScene(tmpScene, tessOptions);
|
||||||
|
|
||||||
if (geoms == null || geoms.Count == 0)
|
if (geoms == null || geoms.Count == 0) {
|
||||||
{
|
|
||||||
Debug.LogWarning($"No geometry generated for color {color} (skipping).");
|
Debug.LogWarning($"No geometry generated for color {color} (skipping).");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -153,15 +139,13 @@ public class SvgToFlatMeshEditor : EditorWindow
|
|||||||
|
|
||||||
var mr = go.AddComponent<MeshRenderer>();
|
var mr = go.AddComponent<MeshRenderer>();
|
||||||
|
|
||||||
if (editorMaterial != null)
|
if (editorMaterial != null) {
|
||||||
{
|
|
||||||
// instantiate a material so each region can have its own color without overwriting the original asset
|
// instantiate a material so each region can have its own color without overwriting the original asset
|
||||||
Material matInstance = new Material(editorMaterial);
|
Material matInstance = new Material(editorMaterial);
|
||||||
matInstance.color = color;
|
matInstance.color = color;
|
||||||
mr.sharedMaterial = matInstance;
|
mr.sharedMaterial = matInstance;
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
{
|
|
||||||
// Create a quick default material
|
// Create a quick default material
|
||||||
var mat = new Material(Shader.Find("Standard"));
|
var mat = new Material(Shader.Find("Standard"));
|
||||||
mat.color = color;
|
mat.color = color;
|
||||||
@@ -185,32 +169,30 @@ public class SvgToFlatMeshEditor : EditorWindow
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Recursively traverse scene nodes and collect filled shapes
|
// Recursively traverse scene nodes and collect filled shapes
|
||||||
void TraverseAndCollectShapes(SceneNode node, Matrix2D parentTransform, Dictionary<Color, List<SceneNodeShapeEntry>> shapesByColor)
|
void TraverseAndCollectShapes(SceneNode node, Matrix2D parentTransform, Dictionary<Color, List<SceneNodeShapeEntry>> shapesByColor) {
|
||||||
{
|
if (node == null) {
|
||||||
if (node == null) return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Combine transforms (VectorGraphics uses Matrix2D)
|
// Combine transforms (VectorGraphics uses Matrix2D)
|
||||||
Matrix2D currentTransform = parentTransform * node.Transform;
|
Matrix2D currentTransform = parentTransform * node.Transform;
|
||||||
|
|
||||||
if (node.Shapes != null && node.Shapes.Count > 0)
|
if (node.Shapes != null && node.Shapes.Count > 0) {
|
||||||
{
|
foreach (var shape in node.Shapes) {
|
||||||
foreach (var shape in node.Shapes)
|
if (shape == null) {
|
||||||
{
|
continue;
|
||||||
if (shape == null) continue;
|
}
|
||||||
// Only treat fills (SolidFill)
|
// Only treat fills (SolidFill)
|
||||||
if (shape.Fill is SolidFill sf)
|
if (shape.Fill is SolidFill sf) {
|
||||||
{
|
|
||||||
Color col = sf.Color;
|
Color col = sf.Color;
|
||||||
// Note: color comes as linear RGBA. Convert to Unity's Color (already same type)
|
// Note: color comes as linear RGBA. Convert to Unity's Color (already same type)
|
||||||
if (!shapesByColor.TryGetValue(col, out List<SceneNodeShapeEntry> list))
|
if (!shapesByColor.TryGetValue(col, out List<SceneNodeShapeEntry> list)) {
|
||||||
{
|
|
||||||
list = new List<SceneNodeShapeEntry>();
|
list = new List<SceneNodeShapeEntry>();
|
||||||
shapesByColor[col] = list;
|
shapesByColor[col] = list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the shape together with a node that carries the proper transform
|
// Store the shape together with a node that carries the proper transform
|
||||||
SceneNode fakeNode = new SceneNode()
|
SceneNode fakeNode = new SceneNode() {
|
||||||
{
|
|
||||||
Transform = currentTransform,
|
Transform = currentTransform,
|
||||||
Shapes = new List<Shape>() { shape }
|
Shapes = new List<Shape>() { shape }
|
||||||
};
|
};
|
||||||
@@ -219,28 +201,27 @@ public class SvgToFlatMeshEditor : EditorWindow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.Children != null && node.Children.Count > 0)
|
if (node.Children != null && node.Children.Count > 0) {
|
||||||
{
|
foreach (var c in node.Children) {
|
||||||
foreach (var c in node.Children)
|
|
||||||
TraverseAndCollectShapes(c, currentTransform, shapesByColor);
|
TraverseAndCollectShapes(c, currentTransform, shapesByColor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a Mesh from VectorUtils.Geometry list
|
// Build a Mesh from VectorUtils.Geometry list
|
||||||
Mesh BuildMeshFromGeometries(List<VectorUtils.Geometry> geoms, Vector2 geomsCenter, float globalScale)
|
Mesh BuildMeshFromGeometries(List<VectorUtils.Geometry> geoms, Vector2 geomsCenter, float globalScale) {
|
||||||
{
|
|
||||||
var verts = new List<Vector3>();
|
var verts = new List<Vector3>();
|
||||||
var uvs = new List<Vector2>();
|
var uvs = new List<Vector2>();
|
||||||
var indices = new List<int>();
|
var indices = new List<int>();
|
||||||
|
|
||||||
int baseIndex = 0;
|
int baseIndex = 0;
|
||||||
foreach (var g in geoms)
|
foreach (var g in geoms) {
|
||||||
{
|
if (g == null || g.Vertices == null || g.Indices == null) {
|
||||||
if (g == null || g.Vertices == null || g.Indices == null) continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Add vertices (VectorUtils uses Vector2 for geometry XY)
|
// Add vertices (VectorUtils uses Vector2 for geometry XY)
|
||||||
for (int i = 0; i < g.Vertices.Length; i++)
|
for (int i = 0; i < g.Vertices.Length; i++) {
|
||||||
{
|
|
||||||
var v2 = g.Vertices[i];
|
var v2 = g.Vertices[i];
|
||||||
|
|
||||||
// Map XY -> XZ plane; Y = 0
|
// Map XY -> XZ plane; Y = 0
|
||||||
@@ -248,15 +229,16 @@ public class SvgToFlatMeshEditor : EditorWindow
|
|||||||
verts.Add(v3);
|
verts.Add(v3);
|
||||||
|
|
||||||
// UVs: If geometry provides UV, use it; otherwise use XY mapped to UV
|
// UVs: If geometry provides UV, use it; otherwise use XY mapped to UV
|
||||||
if (g.UVs != null && g.UVs.Length == g.Vertices.Length)
|
if (g.UVs != null && g.UVs.Length == g.Vertices.Length) {
|
||||||
uvs.Add(g.UVs[i]);
|
uvs.Add(g.UVs[i]);
|
||||||
else
|
}
|
||||||
|
else {
|
||||||
uvs.Add(new Vector2(v2.x, -v2.y));
|
uvs.Add(new Vector2(v2.x, -v2.y));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add indices (triangles)
|
// Add indices (triangles)
|
||||||
for (int i = 0; i < g.Indices.Length; i += 3)
|
for (int i = 0; i < g.Indices.Length; i += 3) {
|
||||||
{
|
|
||||||
// VectorUtils yields triangles in clockwise winding
|
// VectorUtils yields triangles in clockwise winding
|
||||||
// Unity is supposed to use clockwise as well, but in practice we find we need to flip the order to get correct facing.
|
// Unity is supposed to use clockwise as well, but in practice we find we need to flip the order to get correct facing.
|
||||||
indices.Add(baseIndex + g.Indices[i + 1]);
|
indices.Add(baseIndex + g.Indices[i + 1]);
|
||||||
@@ -282,34 +264,28 @@ public class SvgToFlatMeshEditor : EditorWindow
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper to produce a safe string for color names
|
// Helper to produce a safe string for color names
|
||||||
string ColorToName(Color c)
|
string ColorToName(Color c) {
|
||||||
{
|
|
||||||
// Try to present RGBA hex
|
// Try to present RGBA hex
|
||||||
Color32 cc = c;
|
Color32 cc = c;
|
||||||
return $"{cc.r:X2}{cc.g:X2}{cc.b:X2}{(cc.a < 255 ? cc.a.ToString("X2") : "")}";
|
return $"{cc.r:X2}{cc.g:X2}{cc.b:X2}{(cc.a < 255 ? cc.a.ToString("X2") : "")}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small helper type to keep a shape and associated node (with transform)
|
// Small helper type to keep a shape and associated node (with transform)
|
||||||
class SceneNodeShapeEntry
|
class SceneNodeShapeEntry {
|
||||||
{
|
|
||||||
public SceneNode Node;
|
public SceneNode Node;
|
||||||
public Shape Shape;
|
public Shape Shape;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple color comparer for use as Dictionary key
|
// Simple color comparer for use as Dictionary key
|
||||||
class ColorEqualityComparer : IEqualityComparer<Color>
|
class ColorEqualityComparer : IEqualityComparer<Color> {
|
||||||
{
|
public bool Equals(Color x, Color y) {
|
||||||
public bool Equals(Color x, Color y)
|
|
||||||
{
|
|
||||||
// Compare with exactness; you could add tolerance if you want near-colors grouped
|
// Compare with exactness; you could add tolerance if you want near-colors grouped
|
||||||
return Mathf.Approximately(x.r, y.r) && Mathf.Approximately(x.g, y.g) &&
|
return Mathf.Approximately(x.r, y.r) && Mathf.Approximately(x.g, y.g) &&
|
||||||
Mathf.Approximately(x.b, y.b) && Mathf.Approximately(x.a, y.a);
|
Mathf.Approximately(x.b, y.b) && Mathf.Approximately(x.a, y.a);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetHashCode(Color obj)
|
public int GetHashCode(Color obj) {
|
||||||
{
|
unchecked {
|
||||||
unchecked
|
|
||||||
{
|
|
||||||
int hash = 17;
|
int hash = 17;
|
||||||
hash = hash * 23 + Mathf.RoundToInt(obj.r * 255f);
|
hash = hash * 23 + Mathf.RoundToInt(obj.r * 255f);
|
||||||
hash = hash * 23 + Mathf.RoundToInt(obj.g * 255f);
|
hash = hash * 23 + Mathf.RoundToInt(obj.g * 255f);
|
||||||
|
|||||||
Reference in New Issue
Block a user