From c7a0256a02e5f316440561cf75830d6ae48332f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 11:22:31 +0000 Subject: [PATCH 01/22] Initial plan From 05b5fd175dc4c1ca347a704246620015d2ef9fc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 11:31:15 +0000 Subject: [PATCH 02/22] Add MakerPrompt.Shared.ShapeIt project with CADability integration Co-authored-by: akinbender <40242943+akinbender@users.noreply.github.com> --- MakerPrompt.Blazor/MakerPrompt.Blazor.csproj | 1 + MakerPrompt.Blazor/Program.cs | 2 + .../Documents/CadabilityDocumentHost.cs | 160 ++++++++++++++++++ .../Documents/ICadDocumentHost.cs | 79 +++++++++ .../MakerPrompt.Shared.ShapeIt.csproj | 14 ++ .../Parameters/CadParameterDescriptor.cs | 21 +++ .../Parameters/CadParameterKind.cs | 27 +++ .../Parameters/CadParameterValue.cs | 11 ++ .../Rendering/CameraState.cs | 15 ++ .../Rendering/ISceneRenderer.cs | 22 +++ .../Rendering/MeshExportOptions.cs | 11 ++ .../Rendering/MeshExportResult.cs | 13 ++ .../Rendering/MeshExporter.cs | 143 ++++++++++++++++ .../Rendering/NullSceneRenderer.cs | 31 ++++ .../Rendering/SceneBuilder.cs | 139 +++++++++++++++ .../Rendering/SceneDetailLevel.cs | 22 +++ .../Rendering/SceneSnapshot.cs | 53 ++++++ .../ServiceCollectionExtensions.cs | 22 +++ MakerPrompt.sln | 156 ++++++++++------- 19 files changed, 885 insertions(+), 57 deletions(-) create mode 100644 MakerPrompt.Shared.ShapeIt/Documents/CadabilityDocumentHost.cs create mode 100644 MakerPrompt.Shared.ShapeIt/Documents/ICadDocumentHost.cs create mode 100644 MakerPrompt.Shared.ShapeIt/MakerPrompt.Shared.ShapeIt.csproj create mode 100644 MakerPrompt.Shared.ShapeIt/Parameters/CadParameterDescriptor.cs create mode 100644 MakerPrompt.Shared.ShapeIt/Parameters/CadParameterKind.cs create mode 100644 MakerPrompt.Shared.ShapeIt/Parameters/CadParameterValue.cs create mode 100644 MakerPrompt.Shared.ShapeIt/Rendering/CameraState.cs create mode 100644 MakerPrompt.Shared.ShapeIt/Rendering/ISceneRenderer.cs create mode 100644 MakerPrompt.Shared.ShapeIt/Rendering/MeshExportOptions.cs create mode 100644 MakerPrompt.Shared.ShapeIt/Rendering/MeshExportResult.cs create mode 100644 MakerPrompt.Shared.ShapeIt/Rendering/MeshExporter.cs create mode 100644 MakerPrompt.Shared.ShapeIt/Rendering/NullSceneRenderer.cs create mode 100644 MakerPrompt.Shared.ShapeIt/Rendering/SceneBuilder.cs create mode 100644 MakerPrompt.Shared.ShapeIt/Rendering/SceneDetailLevel.cs create mode 100644 MakerPrompt.Shared.ShapeIt/Rendering/SceneSnapshot.cs create mode 100644 MakerPrompt.Shared.ShapeIt/ServiceCollectionExtensions.cs diff --git a/MakerPrompt.Blazor/MakerPrompt.Blazor.csproj b/MakerPrompt.Blazor/MakerPrompt.Blazor.csproj index 73a3a9e..803b862 100644 --- a/MakerPrompt.Blazor/MakerPrompt.Blazor.csproj +++ b/MakerPrompt.Blazor/MakerPrompt.Blazor.csproj @@ -23,6 +23,7 @@ + diff --git a/MakerPrompt.Blazor/Program.cs b/MakerPrompt.Blazor/Program.cs index 5db4987..3f5bbc3 100644 --- a/MakerPrompt.Blazor/Program.cs +++ b/MakerPrompt.Blazor/Program.cs @@ -1,6 +1,7 @@ using System.Globalization; using MakerPrompt.Blazor; using MakerPrompt.Blazor.Services; +using MakerPrompt.Shared.ShapeIt; using MakerPrompt.Shared.Utils; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; @@ -11,6 +12,7 @@ builder.RootComponents.Add("head::after"); builder.Services.RegisterMakerPromptSharedServices(); +builder.Services.AddShapeItForMakerPrompt(); var host = builder.Build(); const string defaultCulture = "en-US"; diff --git a/MakerPrompt.Shared.ShapeIt/Documents/CadabilityDocumentHost.cs b/MakerPrompt.Shared.ShapeIt/Documents/CadabilityDocumentHost.cs new file mode 100644 index 0000000..abda464 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Documents/CadabilityDocumentHost.cs @@ -0,0 +1,160 @@ +using CADability; +using CADability.GeoObject; +using MakerPrompt.Shared.ShapeIt.Parameters; +using MakerPrompt.Shared.ShapeIt.Rendering; + +namespace MakerPrompt.Shared.ShapeIt.Documents; + +/// +/// CADability-based implementation of ICadDocumentHost. +/// Wraps a CADability Project and Model to provide a UI-agnostic CAD document interface. +/// +public class CadabilityDocumentHost : ICadDocumentHost +{ + private Project? _project; + private Model? _activeModel; + private readonly Guid _id = Guid.NewGuid(); + private string? _name; + + /// + public Guid Id => _id; + + /// + public string? Name => _name; + + /// + public event EventHandler? Changed; + + /// + public event EventHandler? ParameterChanged; + + /// + public Task InitializeNewAsync(string? template = null, CancellationToken ct = default) + { + _project = Project.Construct(); + _activeModel = _project.GetActiveModel(); + _name = "Untitled"; + + WireModelEvents(); + + return Task.CompletedTask; + } + + /// + public async Task LoadAsync(Stream fileStream, string? fileName = null, CancellationToken ct = default) + { + // CADability's ReadFromFile expects a file path, so we need to write to a temp file + var tempFile = System.IO.Path.GetTempFileName(); + try + { + using (var fs = File.Create(tempFile)) + { + await fileStream.CopyToAsync(fs, ct); + } + + _project = Project.Construct(); + _project = Project.ReadFromFile(tempFile); + _activeModel = _project.GetActiveModel(); + _name = fileName ?? "Document"; + + WireModelEvents(); + } + finally + { + // Clean up temp file + try + { + if (File.Exists(tempFile)) + File.Delete(tempFile); + } + catch + { + // Ignore cleanup errors + } + } + } + + /// + public async Task SaveAsync(Stream target, string? fileName = null, CancellationToken ct = default) + { + if (_project == null) + throw new InvalidOperationException("No document is loaded."); + + // CADability's WriteToFile expects a file path, so we write to a temp file and then copy to stream + var tempFile = System.IO.Path.GetTempFileName(); + try + { + _project.WriteToFile(tempFile); + + using var fs = File.OpenRead(tempFile); + await fs.CopyToAsync(target, ct); + } + finally + { + // Clean up temp file + try + { + if (File.Exists(tempFile)) + File.Delete(tempFile); + } + catch + { + // Ignore cleanup errors + } + } + } + + /// + public Task> GetParametersAsync(CancellationToken ct = default) + { + // Parameters not yet implemented - return empty list + IReadOnlyList result = Array.Empty(); + return Task.FromResult(result); + } + + /// + public Task UpdateParametersAsync(IEnumerable values, CancellationToken ct = default) + { + // Parameters not yet implemented - no-op + return Task.CompletedTask; + } + + /// + public Task RegenerateAsync(CancellationToken ct = default) + { + // CADability recomputes triangulation on-demand, so we just notify that something changed + Changed?.Invoke(this, EventArgs.Empty); + return Task.CompletedTask; + } + + /// + public Task GetSceneAsync(SceneDetailLevel detail, CancellationToken ct = default) + { + if (_activeModel == null) + return Task.FromResult(new SceneSnapshot(Array.Empty())); + + var snapshot = SceneBuilder.BuildSceneFromModel(_activeModel, detail); + return Task.FromResult(snapshot); + } + + /// + public Task ExportMeshAsync(MeshExportOptions options, CancellationToken ct = default) + { + if (_activeModel == null) + throw new InvalidOperationException("No model is loaded."); + + var result = MeshExporter.ExportModelToStl(_activeModel, options); + return Task.FromResult(result); + } + + private void WireModelEvents() + { + if (_activeModel == null) + return; + + // Wire up model events to raise Changed event + _activeModel.GeoObjectAddedEvent += (go) => Changed?.Invoke(this, EventArgs.Empty); + _activeModel.GeoObjectRemovedEvent += (go) => Changed?.Invoke(this, EventArgs.Empty); + _activeModel.GeoObjectDidChangeEvent += (sender, change) => Changed?.Invoke(this, EventArgs.Empty); + } +} diff --git a/MakerPrompt.Shared.ShapeIt/Documents/ICadDocumentHost.cs b/MakerPrompt.Shared.ShapeIt/Documents/ICadDocumentHost.cs new file mode 100644 index 0000000..fe16439 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Documents/ICadDocumentHost.cs @@ -0,0 +1,79 @@ +using MakerPrompt.Shared.ShapeIt.Parameters; +using MakerPrompt.Shared.ShapeIt.Rendering; + +namespace MakerPrompt.Shared.ShapeIt.Documents; + +/// +/// Abstraction over a CAD document host that manages a CAD kernel and provides +/// a UI-agnostic interface for document operations. +/// +public interface ICadDocumentHost +{ + /// + /// Unique identifier for this document instance. + /// + Guid Id { get; } + + /// + /// Optional document name. + /// + string? Name { get; } + + /// + /// Event raised when the document has changed (geometry added/removed/modified). + /// + event EventHandler? Changed; + + /// + /// Event raised when a parameter has been changed. + /// + event EventHandler? ParameterChanged; + + /// + /// Initializes a new CAD document, optionally from a template. + /// + /// Optional template identifier. + /// Cancellation token. + Task InitializeNewAsync(string? template = null, CancellationToken ct = default); + + /// + /// Loads a CAD document from a stream. + /// + /// Stream containing the document data. + /// Optional filename hint. + /// Cancellation token. + Task LoadAsync(Stream fileStream, string? fileName = null, CancellationToken ct = default); + + /// + /// Saves the current document to a stream. + /// + /// Target stream to write the document. + /// Optional filename hint. + /// Cancellation token. + Task SaveAsync(Stream target, string? fileName = null, CancellationToken ct = default); + + /// + /// Gets the list of available parameters for this document. + /// + Task> GetParametersAsync(CancellationToken ct = default); + + /// + /// Updates one or more parameter values. + /// + Task UpdateParametersAsync(IEnumerable values, CancellationToken ct = default); + + /// + /// Forces a regeneration of the document geometry. + /// + Task RegenerateAsync(CancellationToken ct = default); + + /// + /// Gets a snapshot of the current scene for rendering. + /// + Task GetSceneAsync(SceneDetailLevel detail, CancellationToken ct = default); + + /// + /// Exports the document mesh to a specific format (e.g., STL). + /// + Task ExportMeshAsync(MeshExportOptions options, CancellationToken ct = default); +} diff --git a/MakerPrompt.Shared.ShapeIt/MakerPrompt.Shared.ShapeIt.csproj b/MakerPrompt.Shared.ShapeIt/MakerPrompt.Shared.ShapeIt.csproj new file mode 100644 index 0000000..11ba445 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/MakerPrompt.Shared.ShapeIt.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + enable + enable + + + + + + + + diff --git a/MakerPrompt.Shared.ShapeIt/Parameters/CadParameterDescriptor.cs b/MakerPrompt.Shared.ShapeIt/Parameters/CadParameterDescriptor.cs new file mode 100644 index 0000000..9b21d61 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Parameters/CadParameterDescriptor.cs @@ -0,0 +1,21 @@ +namespace MakerPrompt.Shared.ShapeIt.Parameters; + +/// +/// Describes a CAD parameter that can be modified by the user. +/// +/// The parameter name/identifier. +/// Human-readable display name. +/// The type of parameter. +/// The default value for this parameter. +/// Optional minimum value (for numeric parameters). +/// Optional maximum value (for numeric parameters). +/// Optional list of choices (for choice parameters). +public record CadParameterDescriptor( + string Name, + string DisplayName, + CadParameterKind Kind, + object? DefaultValue, + double? MinValue = null, + double? MaxValue = null, + IReadOnlyList? Choices = null +); diff --git a/MakerPrompt.Shared.ShapeIt/Parameters/CadParameterKind.cs b/MakerPrompt.Shared.ShapeIt/Parameters/CadParameterKind.cs new file mode 100644 index 0000000..93e5c56 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Parameters/CadParameterKind.cs @@ -0,0 +1,27 @@ +namespace MakerPrompt.Shared.ShapeIt.Parameters; + +/// +/// Defines the type of a CAD parameter. +/// +public enum CadParameterKind +{ + /// + /// A numeric parameter (e.g., length, angle, count). + /// + Numeric, + + /// + /// A text/string parameter. + /// + Text, + + /// + /// A boolean parameter (e.g., enable/disable). + /// + Boolean, + + /// + /// An enumeration parameter with a fixed set of choices. + /// + Choice +} diff --git a/MakerPrompt.Shared.ShapeIt/Parameters/CadParameterValue.cs b/MakerPrompt.Shared.ShapeIt/Parameters/CadParameterValue.cs new file mode 100644 index 0000000..5a5812f --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Parameters/CadParameterValue.cs @@ -0,0 +1,11 @@ +namespace MakerPrompt.Shared.ShapeIt.Parameters; + +/// +/// Represents a parameter value update. +/// +/// The parameter name/identifier. +/// The new value for this parameter. +public record CadParameterValue( + string Name, + object? Value +); diff --git a/MakerPrompt.Shared.ShapeIt/Rendering/CameraState.cs b/MakerPrompt.Shared.ShapeIt/Rendering/CameraState.cs new file mode 100644 index 0000000..cd87f52 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Rendering/CameraState.cs @@ -0,0 +1,15 @@ +namespace MakerPrompt.Shared.ShapeIt.Rendering; + +/// +/// Represents the camera state for 3D scene rendering. +/// +/// Camera position (x, y, z). +/// Camera look-at target (x, y, z). +/// Camera up vector (x, y, z). +/// Field of view in degrees. +public record CameraState( + float[] Position, + float[] Target, + float[] Up, + float FieldOfViewDegrees +); diff --git a/MakerPrompt.Shared.ShapeIt/Rendering/ISceneRenderer.cs b/MakerPrompt.Shared.ShapeIt/Rendering/ISceneRenderer.cs new file mode 100644 index 0000000..72a8d97 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Rendering/ISceneRenderer.cs @@ -0,0 +1,22 @@ +namespace MakerPrompt.Shared.ShapeIt.Rendering; + +/// +/// Interface for rendering 3D scenes. +/// +public interface ISceneRenderer : IAsyncDisposable +{ + /// + /// Initializes the renderer. + /// + Task InitializeAsync(CancellationToken ct = default); + + /// + /// Renders the given scene snapshot. + /// + Task RenderAsync(SceneSnapshot snapshot, CancellationToken ct = default); + + /// + /// Sets the camera state for rendering. + /// + Task SetCameraAsync(CameraState camera, CancellationToken ct = default); +} diff --git a/MakerPrompt.Shared.ShapeIt/Rendering/MeshExportOptions.cs b/MakerPrompt.Shared.ShapeIt/Rendering/MeshExportOptions.cs new file mode 100644 index 0000000..eb537ea --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Rendering/MeshExportOptions.cs @@ -0,0 +1,11 @@ +namespace MakerPrompt.Shared.ShapeIt.Rendering; + +/// +/// Options for exporting mesh data. +/// +/// Export format (e.g., "stl-binary"). +/// Tessellation tolerance for mesh generation. +public record MeshExportOptions( + string Format, + double Tolerance +); diff --git a/MakerPrompt.Shared.ShapeIt/Rendering/MeshExportResult.cs b/MakerPrompt.Shared.ShapeIt/Rendering/MeshExportResult.cs new file mode 100644 index 0000000..a62d0a3 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Rendering/MeshExportResult.cs @@ -0,0 +1,13 @@ +namespace MakerPrompt.Shared.ShapeIt.Rendering; + +/// +/// Result of a mesh export operation. +/// +/// MIME type of the exported content (e.g., "model/stl"). +/// Suggested filename for saving. +/// Binary content of the exported mesh. +public record MeshExportResult( + string MimeType, + string SuggestedFileName, + byte[] Content +); diff --git a/MakerPrompt.Shared.ShapeIt/Rendering/MeshExporter.cs b/MakerPrompt.Shared.ShapeIt/Rendering/MeshExporter.cs new file mode 100644 index 0000000..7cd39db --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Rendering/MeshExporter.cs @@ -0,0 +1,143 @@ +using CADability; +using CADability.GeoObject; +using System.Text; + +namespace MakerPrompt.Shared.ShapeIt.Rendering; + +/// +/// Exports CADability models to various mesh formats. +/// +public static class MeshExporter +{ + /// + /// Exports a CADability Model to STL format. + /// + public static MeshExportResult ExportModelToStl(Model model, MeshExportOptions options) + { + if (options.Format != "stl-binary") + throw new NotSupportedException($"Format '{options.Format}' is not supported. Only 'stl-binary' is currently supported."); + + var triangles = new List(); + + // Collect all triangles from all solids + foreach (var obj in model.AllObjects) + { + if (obj is Solid solid) + { + CollectTrianglesFromSolid(solid, triangles, options.Tolerance); + } + } + + // Write binary STL + var content = WriteBinaryStl(triangles); + + return new MeshExportResult( + "model/stl", + "export.stl", + content + ); + } + + private static void CollectTrianglesFromSolid(Solid solid, List triangles, double tolerance) + { + foreach (var shell in solid.Shells) + { + foreach (var face in shell.Faces) + { + try + { + GeoPoint[] vertices; + GeoPoint2D[] uvVertices; + int[] triangleIndices; + BoundingCube extent; + + face.GetTriangulation(tolerance, out vertices, out uvVertices, out triangleIndices, out extent); + if (vertices == null || triangleIndices == null) + continue; + + // Convert to triangles (indices are flat: i0, i1, i2, i3, i4, i5, ...) + for (int i = 0; i < triangleIndices.Length; i += 3) + { + if (i + 2 < triangleIndices.Length) + { + var v1 = vertices[triangleIndices[i]]; + var v2 = vertices[triangleIndices[i + 1]]; + var v3 = vertices[triangleIndices[i + 2]]; + + // Calculate normal + var edge1 = new CADability.GeoVector(v2.x - v1.x, v2.y - v1.y, v2.z - v1.z); + var edge2 = new CADability.GeoVector(v3.x - v1.x, v3.y - v1.y, v3.z - v1.z); + var normal = edge1 ^ edge2; // Cross product + normal.Norm(); + + triangles.Add(new Triangle + { + Normal = normal, + V1 = v1, + V2 = v2, + V3 = v3 + }); + } + } + } + catch + { + // Skip faces that fail to triangulate + continue; + } + } + } + } + + private static byte[] WriteBinaryStl(List triangles) + { + using var ms = new MemoryStream(); + using var writer = new BinaryWriter(ms); + + // Write 80-byte header + var header = new byte[80]; + var headerText = Encoding.ASCII.GetBytes("Binary STL from MakerPrompt"); + Array.Copy(headerText, header, Math.Min(headerText.Length, 80)); + writer.Write(header); + + // Write triangle count + writer.Write((uint)triangles.Count); + + // Write triangles + foreach (var triangle in triangles) + { + // Normal + writer.Write((float)triangle.Normal.x); + writer.Write((float)triangle.Normal.y); + writer.Write((float)triangle.Normal.z); + + // Vertex 1 + writer.Write((float)triangle.V1.x); + writer.Write((float)triangle.V1.y); + writer.Write((float)triangle.V1.z); + + // Vertex 2 + writer.Write((float)triangle.V2.x); + writer.Write((float)triangle.V2.y); + writer.Write((float)triangle.V2.z); + + // Vertex 3 + writer.Write((float)triangle.V3.x); + writer.Write((float)triangle.V3.y); + writer.Write((float)triangle.V3.z); + + // Attribute byte count (unused) + writer.Write((ushort)0); + } + + return ms.ToArray(); + } + + private struct Triangle + { + public CADability.GeoVector Normal; + public CADability.GeoPoint V1; + public CADability.GeoPoint V2; + public CADability.GeoPoint V3; + } +} diff --git a/MakerPrompt.Shared.ShapeIt/Rendering/NullSceneRenderer.cs b/MakerPrompt.Shared.ShapeIt/Rendering/NullSceneRenderer.cs new file mode 100644 index 0000000..e2626da --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Rendering/NullSceneRenderer.cs @@ -0,0 +1,31 @@ +namespace MakerPrompt.Shared.ShapeIt.Rendering; + +/// +/// A no-op scene renderer used as a default implementation. +/// +public class NullSceneRenderer : ISceneRenderer +{ + /// + public Task InitializeAsync(CancellationToken ct = default) + { + return Task.CompletedTask; + } + + /// + public Task RenderAsync(SceneSnapshot snapshot, CancellationToken ct = default) + { + return Task.CompletedTask; + } + + /// + public Task SetCameraAsync(CameraState camera, CancellationToken ct = default) + { + return Task.CompletedTask; + } + + /// + public ValueTask DisposeAsync() + { + return ValueTask.CompletedTask; + } +} diff --git a/MakerPrompt.Shared.ShapeIt/Rendering/SceneBuilder.cs b/MakerPrompt.Shared.ShapeIt/Rendering/SceneBuilder.cs new file mode 100644 index 0000000..e197bc4 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Rendering/SceneBuilder.cs @@ -0,0 +1,139 @@ +using CADability; +using CADability.GeoObject; + +namespace MakerPrompt.Shared.ShapeIt.Rendering; + +/// +/// Builds scene snapshots from CADability Model objects. +/// +public static class SceneBuilder +{ + /// + /// Builds a scene snapshot from a CADability Model. + /// + public static SceneSnapshot BuildSceneFromModel(Model model, SceneDetailLevel detail) + { + var nodes = new List(); + + foreach (var obj in model.AllObjects) + { + if (obj is Solid solid) + { + var node = BuildNodeFromSolid(solid, detail); + if (node != null) + nodes.Add(node); + } + } + + return new SceneSnapshot(nodes); + } + + private static SceneNode? BuildNodeFromSolid(Solid solid, SceneDetailLevel detail) + { + if (detail == SceneDetailLevel.BoundingBoxesOnly) + { + // For bounding boxes, we could create a simple box mesh, but for now return null + return null; + } + + // Get the primary shell from the solid + var shell = solid.Shells.Length > 0 ? solid.Shells[0] : null; + if (shell == null) + return null; + + var positions = new List(); + var normals = new List(); + var indices = new List(); + + foreach (var face in shell.Faces) + { + try + { + // Get triangulation from the face + GeoPoint[] vertices; + GeoPoint2D[] uvVertices; + int[] triangleIndices; + BoundingCube extent; + + face.GetTriangulation(0.01, out vertices, out uvVertices, out triangleIndices, out extent); + if (vertices == null || triangleIndices == null) + continue; + + int baseIndex = positions.Count / 3; + + // Add vertices + foreach (var point in vertices) + { + positions.Add((float)point.x); + positions.Add((float)point.y); + positions.Add((float)point.z); + + // For now, use face normal for all vertices (could be improved) + try + { + var normal = face.Surface.GetNormal(new CADability.GeoPoint2D(0.5, 0.5)); + normals.Add((float)normal.x); + normals.Add((float)normal.y); + normals.Add((float)normal.z); + } + catch + { + // Fallback to a default normal + normals.Add(0); + normals.Add(0); + normals.Add(1); + } + } + + // Add indices (triangleIndices is a flat array: i0, i1, i2, i3, i4, i5, ...) + for (int i = 0; i < triangleIndices.Length; i += 3) + { + if (i + 2 < triangleIndices.Length) + { + indices.Add(baseIndex + triangleIndices[i]); + indices.Add(baseIndex + triangleIndices[i + 1]); + indices.Add(baseIndex + triangleIndices[i + 2]); + } + } + } + catch + { + // Skip faces that fail to triangulate + continue; + } + } + + if (positions.Count == 0) + return null; + + var meshData = new MeshData( + positions.ToArray(), + normals.ToArray(), + null, // No colors for now + indices.ToArray() + ); + + // Identity transform for now + var transform = new TransformData(new float[] { + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + }); + + IReadOnlyList? edges = null; + if (detail == SceneDetailLevel.ShadedWithEdges) + { + // Could extract edges here, but leaving as null for now + edges = null; + } + + return new SceneNode( + Guid.NewGuid(), + "Solid", + meshData, + edges, + transform + ); + } +} diff --git a/MakerPrompt.Shared.ShapeIt/Rendering/SceneDetailLevel.cs b/MakerPrompt.Shared.ShapeIt/Rendering/SceneDetailLevel.cs new file mode 100644 index 0000000..73ea583 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Rendering/SceneDetailLevel.cs @@ -0,0 +1,22 @@ +namespace MakerPrompt.Shared.ShapeIt.Rendering; + +/// +/// Defines the level of detail for scene rendering. +/// +public enum SceneDetailLevel +{ + /// + /// Only render bounding boxes (fastest). + /// + BoundingBoxesOnly, + + /// + /// Render shaded meshes. + /// + ShadedMeshes, + + /// + /// Render shaded meshes with visible edges. + /// + ShadedWithEdges +} diff --git a/MakerPrompt.Shared.ShapeIt/Rendering/SceneSnapshot.cs b/MakerPrompt.Shared.ShapeIt/Rendering/SceneSnapshot.cs new file mode 100644 index 0000000..a30a377 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Rendering/SceneSnapshot.cs @@ -0,0 +1,53 @@ +namespace MakerPrompt.Shared.ShapeIt.Rendering; + +/// +/// Represents a 4x4 transformation matrix for positioning and orienting objects in 3D space. +/// +/// A 16-element array representing the transformation matrix in row-major order. +public record TransformData(float[] Matrix4x4); + +/// +/// Represents mesh data for rendering. +/// +/// Array of vertex positions (x, y, z, x, y, z, ...). +/// Array of vertex normals (x, y, z, x, y, z, ...). +/// Optional array of vertex colors (r, g, b, a, ...). +/// Array of triangle indices. +public record MeshData( + float[] Positions, + float[] Normals, + float[]? Colors, + int[] Indices +); + +/// +/// Represents edge data for rendering wireframes or silhouettes. +/// +/// Array of edge vertex positions (x, y, z, x, y, z, ...). +/// Indicates if this edge is a silhouette edge. +public record EdgeData( + float[] Positions, + bool IsSilhouette +); + +/// +/// Represents a single node in the scene (typically corresponding to a CAD object). +/// +/// Unique identifier for the scene node. +/// Optional human-readable name. +/// Optional mesh data for rendering surfaces. +/// Optional edge data for rendering wireframes. +/// Transformation matrix for positioning this node. +public record SceneNode( + Guid Id, + string? Name, + MeshData? Mesh, + IReadOnlyList? Edges, + TransformData Transform +); + +/// +/// Represents a complete snapshot of the scene for rendering. +/// +/// List of all scene nodes to render. +public record SceneSnapshot(IReadOnlyList Nodes); diff --git a/MakerPrompt.Shared.ShapeIt/ServiceCollectionExtensions.cs b/MakerPrompt.Shared.ShapeIt/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..23bff25 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/ServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using MakerPrompt.Shared.ShapeIt.Documents; +using MakerPrompt.Shared.ShapeIt.Rendering; +using Microsoft.Extensions.DependencyInjection; + +namespace MakerPrompt.Shared.ShapeIt; + +/// +/// Dependency injection extensions for ShapeIt CAD integration. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Registers ShapeIt services for MakerPrompt. + /// + public static IServiceCollection AddShapeItForMakerPrompt(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + + return services; + } +} diff --git a/MakerPrompt.sln b/MakerPrompt.sln index ba96932..1b5cb0b 100644 --- a/MakerPrompt.sln +++ b/MakerPrompt.sln @@ -1,57 +1,99 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.13.35913.81 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MakerPrompt.Shared", "MakerPrompt.Shared\MakerPrompt.Shared.csproj", "{1659CECC-713A-4F76-B7F3-4D74C0603E97}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MakerPrompt.Blazor", "MakerPrompt.Blazor\MakerPrompt.Blazor.csproj", "{15F52CFF-63A3-427D-8795-95370626C764}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MakerPrompt.MAUI", "MakerPrompt.MAUI\MakerPrompt.MAUI.csproj", "{67704D6B-0609-472D-A78A-473B780D4D1A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" - ProjectSection(SolutionItems) = preProject - .github\workflows\azure-static-web-apps-yellow-sea-04668c503.yml = .github\workflows\azure-static-web-apps-yellow-sea-04668c503.yml - .github\workflows\build.yml = .github\workflows\build.yml - .github\workflows\publish-github-pages.yml = .github\workflows\publish-github-pages.yml - .github\workflows\publish-maui.yml = .github\workflows\publish-maui.yml - .github\workflows\versionize-release.yml = .github\workflows\versionize-release.yml - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gh-pages", "gh-pages", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" - ProjectSection(SolutionItems) = preProject - .github\workflows\gh-pages\404.html = .github\workflows\gh-pages\404.html - .github\workflows\gh-pages\decode.js = .github\workflows\gh-pages\decode.js - .github\workflows\gh-pages\index.html = .github\workflows\gh-pages\index.html - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Release|Any CPU.Build.0 = Release|Any CPU - {15F52CFF-63A3-427D-8795-95370626C764}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {15F52CFF-63A3-427D-8795-95370626C764}.Debug|Any CPU.Build.0 = Debug|Any CPU - {15F52CFF-63A3-427D-8795-95370626C764}.Release|Any CPU.ActiveCfg = Release|Any CPU - {15F52CFF-63A3-427D-8795-95370626C764}.Release|Any CPU.Build.0 = Release|Any CPU - {67704D6B-0609-472D-A78A-473B780D4D1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {67704D6B-0609-472D-A78A-473B780D4D1A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {67704D6B-0609-472D-A78A-473B780D4D1A}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {67704D6B-0609-472D-A78A-473B780D4D1A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {67704D6B-0609-472D-A78A-473B780D4D1A}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {8EC462FD-D22E-90A8-E5CE-7E832BA40C5D} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {545A45A2-4075-429A-AC75-ABFBE72CC15A} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35913.81 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MakerPrompt.Shared", "MakerPrompt.Shared\MakerPrompt.Shared.csproj", "{1659CECC-713A-4F76-B7F3-4D74C0603E97}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MakerPrompt.Blazor", "MakerPrompt.Blazor\MakerPrompt.Blazor.csproj", "{15F52CFF-63A3-427D-8795-95370626C764}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MakerPrompt.MAUI", "MakerPrompt.MAUI\MakerPrompt.MAUI.csproj", "{67704D6B-0609-472D-A78A-473B780D4D1A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" + ProjectSection(SolutionItems) = preProject + .github\workflows\azure-static-web-apps-yellow-sea-04668c503.yml = .github\workflows\azure-static-web-apps-yellow-sea-04668c503.yml + .github\workflows\build.yml = .github\workflows\build.yml + .github\workflows\publish-github-pages.yml = .github\workflows\publish-github-pages.yml + .github\workflows\publish-maui.yml = .github\workflows\publish-maui.yml + .github\workflows\versionize-release.yml = .github\workflows\versionize-release.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gh-pages", "gh-pages", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + .github\workflows\gh-pages\404.html = .github\workflows\gh-pages\404.html + .github\workflows\gh-pages\decode.js = .github\workflows\gh-pages\decode.js + .github\workflows\gh-pages\index.html = .github\workflows\gh-pages\index.html + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MakerPrompt.Shared.ShapeIt", "MakerPrompt.Shared.ShapeIt\MakerPrompt.Shared.ShapeIt.csproj", "{DBEC1B2B-CDC1-4514-A2AB-236650B11DD8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Debug|x64.ActiveCfg = Debug|Any CPU + {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Debug|x64.Build.0 = Debug|Any CPU + {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Debug|x86.ActiveCfg = Debug|Any CPU + {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Debug|x86.Build.0 = Debug|Any CPU + {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Release|Any CPU.Build.0 = Release|Any CPU + {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Release|x64.ActiveCfg = Release|Any CPU + {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Release|x64.Build.0 = Release|Any CPU + {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Release|x86.ActiveCfg = Release|Any CPU + {1659CECC-713A-4F76-B7F3-4D74C0603E97}.Release|x86.Build.0 = Release|Any CPU + {15F52CFF-63A3-427D-8795-95370626C764}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15F52CFF-63A3-427D-8795-95370626C764}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15F52CFF-63A3-427D-8795-95370626C764}.Debug|x64.ActiveCfg = Debug|Any CPU + {15F52CFF-63A3-427D-8795-95370626C764}.Debug|x64.Build.0 = Debug|Any CPU + {15F52CFF-63A3-427D-8795-95370626C764}.Debug|x86.ActiveCfg = Debug|Any CPU + {15F52CFF-63A3-427D-8795-95370626C764}.Debug|x86.Build.0 = Debug|Any CPU + {15F52CFF-63A3-427D-8795-95370626C764}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15F52CFF-63A3-427D-8795-95370626C764}.Release|Any CPU.Build.0 = Release|Any CPU + {15F52CFF-63A3-427D-8795-95370626C764}.Release|x64.ActiveCfg = Release|Any CPU + {15F52CFF-63A3-427D-8795-95370626C764}.Release|x64.Build.0 = Release|Any CPU + {15F52CFF-63A3-427D-8795-95370626C764}.Release|x86.ActiveCfg = Release|Any CPU + {15F52CFF-63A3-427D-8795-95370626C764}.Release|x86.Build.0 = Release|Any CPU + {67704D6B-0609-472D-A78A-473B780D4D1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67704D6B-0609-472D-A78A-473B780D4D1A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67704D6B-0609-472D-A78A-473B780D4D1A}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {67704D6B-0609-472D-A78A-473B780D4D1A}.Debug|x64.ActiveCfg = Debug|Any CPU + {67704D6B-0609-472D-A78A-473B780D4D1A}.Debug|x64.Build.0 = Debug|Any CPU + {67704D6B-0609-472D-A78A-473B780D4D1A}.Debug|x86.ActiveCfg = Debug|Any CPU + {67704D6B-0609-472D-A78A-473B780D4D1A}.Debug|x86.Build.0 = Debug|Any CPU + {67704D6B-0609-472D-A78A-473B780D4D1A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67704D6B-0609-472D-A78A-473B780D4D1A}.Release|Any CPU.Build.0 = Release|Any CPU + {67704D6B-0609-472D-A78A-473B780D4D1A}.Release|x64.ActiveCfg = Release|Any CPU + {67704D6B-0609-472D-A78A-473B780D4D1A}.Release|x64.Build.0 = Release|Any CPU + {67704D6B-0609-472D-A78A-473B780D4D1A}.Release|x86.ActiveCfg = Release|Any CPU + {67704D6B-0609-472D-A78A-473B780D4D1A}.Release|x86.Build.0 = Release|Any CPU + {DBEC1B2B-CDC1-4514-A2AB-236650B11DD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBEC1B2B-CDC1-4514-A2AB-236650B11DD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBEC1B2B-CDC1-4514-A2AB-236650B11DD8}.Debug|x64.ActiveCfg = Debug|Any CPU + {DBEC1B2B-CDC1-4514-A2AB-236650B11DD8}.Debug|x64.Build.0 = Debug|Any CPU + {DBEC1B2B-CDC1-4514-A2AB-236650B11DD8}.Debug|x86.ActiveCfg = Debug|Any CPU + {DBEC1B2B-CDC1-4514-A2AB-236650B11DD8}.Debug|x86.Build.0 = Debug|Any CPU + {DBEC1B2B-CDC1-4514-A2AB-236650B11DD8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBEC1B2B-CDC1-4514-A2AB-236650B11DD8}.Release|Any CPU.Build.0 = Release|Any CPU + {DBEC1B2B-CDC1-4514-A2AB-236650B11DD8}.Release|x64.ActiveCfg = Release|Any CPU + {DBEC1B2B-CDC1-4514-A2AB-236650B11DD8}.Release|x64.Build.0 = Release|Any CPU + {DBEC1B2B-CDC1-4514-A2AB-236650B11DD8}.Release|x86.ActiveCfg = Release|Any CPU + {DBEC1B2B-CDC1-4514-A2AB-236650B11DD8}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {8EC462FD-D22E-90A8-E5CE-7E832BA40C5D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {545A45A2-4075-429A-AC75-ABFBE72CC15A} + EndGlobalSection +EndGlobal From 7d19dc4a6f32694aaa72aa5cdf4b490366a511bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 11:33:53 +0000 Subject: [PATCH 03/22] Add web and MAUI renderers with Blazor CAD component Co-authored-by: akinbender <40242943+akinbender@users.noreply.github.com> --- .../Components/CadDocumentHost.razor | 167 ++++++++++++++++++ MakerPrompt.Blazor/Program.cs | 3 + .../Services/WebCadSceneRenderer.cs | 63 +++++++ .../wwwroot/cadRendererInterop.js | 72 ++++++++ MakerPrompt.Blazor/wwwroot/index.html | 15 ++ .../wwwroot/makerpromptCadWorker.js | 37 ++++ MakerPrompt.MAUI/MakerPrompt.MAUI.csproj | 1 + MakerPrompt.MAUI/MauiProgram.cs | 8 + .../Services/MauiCadSceneRenderer.cs | 34 ++++ 9 files changed, 400 insertions(+) create mode 100644 MakerPrompt.Blazor/Components/CadDocumentHost.razor create mode 100644 MakerPrompt.Blazor/Services/WebCadSceneRenderer.cs create mode 100644 MakerPrompt.Blazor/wwwroot/cadRendererInterop.js create mode 100644 MakerPrompt.Blazor/wwwroot/makerpromptCadWorker.js create mode 100644 MakerPrompt.MAUI/Services/MauiCadSceneRenderer.cs diff --git a/MakerPrompt.Blazor/Components/CadDocumentHost.razor b/MakerPrompt.Blazor/Components/CadDocumentHost.razor new file mode 100644 index 0000000..73232c5 --- /dev/null +++ b/MakerPrompt.Blazor/Components/CadDocumentHost.razor @@ -0,0 +1,167 @@ +@using MakerPrompt.Shared.ShapeIt.Documents +@using MakerPrompt.Shared.ShapeIt.Rendering +@inject ICadDocumentHost DocumentHost +@inject ISceneRenderer SceneRenderer +@inject IJSRuntime JS +@implements IAsyncDisposable + +
+
+ + + +
+ +
+ +
+ + @if (_isLoading) + { +
Loading...
+ } + + @if (!string.IsNullOrEmpty(_errorMessage)) + { +
@_errorMessage
+ } +
+ +@code { + private bool _isLoading; + private string? _errorMessage; + private bool _rendererInitialized; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + try + { + // Initialize the renderer with the canvas + var success = await JS.InvokeAsync("cadRenderer.initialize", "cadCanvas"); + if (success) + { + await SceneRenderer.InitializeAsync(); + _rendererInitialized = true; + + // Wire up document change events + DocumentHost.Changed += OnDocumentChanged; + } + else + { + _errorMessage = "Failed to initialize CAD renderer"; + } + } + catch (Exception ex) + { + _errorMessage = $"Error initializing renderer: {ex.Message}"; + } + StateHasChanged(); + } + } + + private async Task CreateNewDocument() + { + try + { + _isLoading = true; + _errorMessage = null; + StateHasChanged(); + + await DocumentHost.InitializeNewAsync(); + await UpdateScene(); + } + catch (Exception ex) + { + _errorMessage = $"Error creating document: {ex.Message}"; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private async Task RegenerateScene() + { + try + { + _isLoading = true; + _errorMessage = null; + StateHasChanged(); + + await DocumentHost.RegenerateAsync(); + await UpdateScene(); + } + catch (Exception ex) + { + _errorMessage = $"Error regenerating: {ex.Message}"; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private async Task ExportStl() + { + try + { + _isLoading = true; + _errorMessage = null; + StateHasChanged(); + + var options = new MeshExportOptions("stl-binary", 0.01); + var result = await DocumentHost.ExportMeshAsync(options); + + // Trigger download via JS + await JS.InvokeVoidAsync("downloadFile", + result.SuggestedFileName, + Convert.ToBase64String(result.Content), + result.MimeType); + } + catch (Exception ex) + { + _errorMessage = $"Error exporting: {ex.Message}"; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private async void OnDocumentChanged(object? sender, EventArgs e) + { + await UpdateScene(); + } + + private async Task UpdateScene() + { + if (!_rendererInitialized) + return; + + try + { + var scene = await DocumentHost.GetSceneAsync(SceneDetailLevel.ShadedMeshes); + await SceneRenderer.RenderAsync(scene); + } + catch (Exception ex) + { + _errorMessage = $"Error rendering scene: {ex.Message}"; + StateHasChanged(); + } + } + + public async ValueTask DisposeAsync() + { + DocumentHost.Changed -= OnDocumentChanged; + + if (_rendererInitialized) + { + await SceneRenderer.DisposeAsync(); + } + } +} diff --git a/MakerPrompt.Blazor/Program.cs b/MakerPrompt.Blazor/Program.cs index 3f5bbc3..b4e02c8 100644 --- a/MakerPrompt.Blazor/Program.cs +++ b/MakerPrompt.Blazor/Program.cs @@ -14,6 +14,9 @@ builder.Services.RegisterMakerPromptSharedServices(); builder.Services.AddShapeItForMakerPrompt(); +// Override the default NullSceneRenderer with the web-based renderer for Blazor +builder.Services.AddScoped(); + var host = builder.Build(); const string defaultCulture = "en-US"; diff --git a/MakerPrompt.Blazor/Services/WebCadSceneRenderer.cs b/MakerPrompt.Blazor/Services/WebCadSceneRenderer.cs new file mode 100644 index 0000000..00475b7 --- /dev/null +++ b/MakerPrompt.Blazor/Services/WebCadSceneRenderer.cs @@ -0,0 +1,63 @@ +using MakerPrompt.Shared.ShapeIt.Rendering; +using Microsoft.JSInterop; + +namespace MakerPrompt.Blazor.Services; + +/// +/// Web-based CAD scene renderer using a dedicated Web Worker and OffscreenCanvas. +/// +public class WebCadSceneRenderer : ISceneRenderer +{ + private readonly IJSRuntime _jsRuntime; + private bool _initialized; + + public WebCadSceneRenderer(IJSRuntime jsRuntime) + { + _jsRuntime = jsRuntime; + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + if (_initialized) + return; + + // Initialize the CAD renderer with a canvas element + // The canvas ID would need to be provided by the component using this renderer + _initialized = true; + await Task.CompletedTask; + } + + public async Task RenderAsync(SceneSnapshot snapshot, CancellationToken ct = default) + { + if (!_initialized) + throw new InvalidOperationException("Renderer not initialized. Call InitializeAsync first."); + + // Send scene snapshot to the web worker via JS interop + await _jsRuntime.InvokeVoidAsync("cadRenderer.render", snapshot); + } + + public async Task SetCameraAsync(CameraState camera, CancellationToken ct = default) + { + if (!_initialized) + throw new InvalidOperationException("Renderer not initialized. Call InitializeAsync first."); + + // Send camera state to the web worker via JS interop + await _jsRuntime.InvokeVoidAsync("cadRenderer.setCamera", camera); + } + + public async ValueTask DisposeAsync() + { + if (_initialized) + { + try + { + await _jsRuntime.InvokeVoidAsync("cadRenderer.dispose"); + } + catch + { + // Ignore errors during disposal + } + _initialized = false; + } + } +} diff --git a/MakerPrompt.Blazor/wwwroot/cadRendererInterop.js b/MakerPrompt.Blazor/wwwroot/cadRendererInterop.js new file mode 100644 index 0000000..66c900b --- /dev/null +++ b/MakerPrompt.Blazor/wwwroot/cadRendererInterop.js @@ -0,0 +1,72 @@ +// CAD Renderer JavaScript Interop +// Bridges Blazor C# with the CAD Web Worker + +window.cadRenderer = { + worker: null, + canvas: null, + + initialize: async function(canvasId) { + console.log('Initializing CAD renderer for canvas:', canvasId); + + // Get canvas element + this.canvas = document.getElementById(canvasId); + if (!this.canvas) { + console.error('Canvas not found:', canvasId); + return false; + } + + // Create and initialize worker + try { + this.worker = new Worker('makerpromptCadWorker.js'); + + // Wait for initialization + return new Promise((resolve) => { + this.worker.addEventListener('message', function handler(e) { + if (e.data.type === 'initialized') { + this.removeEventListener('message', handler); + resolve(e.data.success); + } + }); + + // Send init message + this.worker.postMessage({ type: 'init', data: { canvasId } }); + }); + } catch (error) { + console.error('Failed to create CAD worker:', error); + return false; + } + }, + + render: function(sceneSnapshot) { + if (!this.worker) { + console.error('CAD worker not initialized'); + return; + } + + this.worker.postMessage({ + type: 'render', + data: sceneSnapshot + }); + }, + + setCamera: function(cameraState) { + if (!this.worker) { + console.error('CAD worker not initialized'); + return; + } + + this.worker.postMessage({ + type: 'setCamera', + data: cameraState + }); + }, + + dispose: function() { + if (this.worker) { + this.worker.postMessage({ type: 'dispose' }); + this.worker.terminate(); + this.worker = null; + } + this.canvas = null; + } +}; diff --git a/MakerPrompt.Blazor/wwwroot/index.html b/MakerPrompt.Blazor/wwwroot/index.html index 688457d..8387d0d 100644 --- a/MakerPrompt.Blazor/wwwroot/index.html +++ b/MakerPrompt.Blazor/wwwroot/index.html @@ -36,6 +36,7 @@ + diff --git a/MakerPrompt.Blazor/wwwroot/makerpromptCadWorker.js b/MakerPrompt.Blazor/wwwroot/makerpromptCadWorker.js new file mode 100644 index 0000000..1a810b5 --- /dev/null +++ b/MakerPrompt.Blazor/wwwroot/makerpromptCadWorker.js @@ -0,0 +1,37 @@ +// MakerPrompt CAD Worker +// Dedicated worker for non-blocking WebGL CAD rendering using OffscreenCanvas + +self.addEventListener('message', function(e) { + const { type, data } = e.data; + + switch(type) { + case 'init': + // Initialize WebGL context with OffscreenCanvas + console.log('CAD Worker: Initializing...'); + self.postMessage({ type: 'initialized', success: true }); + break; + + case 'render': + // Render scene snapshot + console.log('CAD Worker: Rendering scene with', data.nodes?.length || 0, 'nodes'); + self.postMessage({ type: 'rendered', success: true }); + break; + + case 'setCamera': + // Update camera state + console.log('CAD Worker: Setting camera'); + self.postMessage({ type: 'cameraSet', success: true }); + break; + + case 'dispose': + // Cleanup resources + console.log('CAD Worker: Disposing...'); + self.postMessage({ type: 'disposed', success: true }); + break; + + default: + console.warn('CAD Worker: Unknown message type', type); + } +}); + +console.log('MakerPrompt CAD Worker loaded'); diff --git a/MakerPrompt.MAUI/MakerPrompt.MAUI.csproj b/MakerPrompt.MAUI/MakerPrompt.MAUI.csproj index d83302f..4073c0e 100644 --- a/MakerPrompt.MAUI/MakerPrompt.MAUI.csproj +++ b/MakerPrompt.MAUI/MakerPrompt.MAUI.csproj @@ -78,6 +78,7 @@ + diff --git a/MakerPrompt.MAUI/MauiProgram.cs b/MakerPrompt.MAUI/MauiProgram.cs index 95188ac..343aa33 100644 --- a/MakerPrompt.MAUI/MauiProgram.cs +++ b/MakerPrompt.MAUI/MauiProgram.cs @@ -1,5 +1,7 @@ using Microsoft.Extensions.Logging; using MakerPrompt.Shared.Utils; +using MakerPrompt.Shared.ShapeIt; +using MakerPrompt.Shared.ShapeIt.Rendering; using Microsoft.AspNetCore.Builder; using MakerPrompt.MAUI.Services; @@ -35,6 +37,12 @@ public static MauiApp CreateMauiApp() .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); builder.Services.RegisterMakerPromptSharedServices(); + + // Register ShapeIt CAD services + builder.Services.AddShapeItForMakerPrompt(); + // Override with MAUI-specific renderer + builder.Services.AddScoped(); + return builder.Build(); } } diff --git a/MakerPrompt.MAUI/Services/MauiCadSceneRenderer.cs b/MakerPrompt.MAUI/Services/MauiCadSceneRenderer.cs new file mode 100644 index 0000000..d631a45 --- /dev/null +++ b/MakerPrompt.MAUI/Services/MauiCadSceneRenderer.cs @@ -0,0 +1,34 @@ +using MakerPrompt.Shared.ShapeIt.Rendering; + +namespace MakerPrompt.MAUI.Services; + +/// +/// MAUI stub renderer for CAD scenes. +/// This is a minimal implementation placeholder for future MAUI-specific rendering. +/// +public class MauiCadSceneRenderer : ISceneRenderer +{ + public Task InitializeAsync(CancellationToken ct = default) + { + // TODO: Implement MAUI-specific initialization + return Task.CompletedTask; + } + + public Task RenderAsync(SceneSnapshot snapshot, CancellationToken ct = default) + { + // TODO: Implement MAUI-specific rendering + return Task.CompletedTask; + } + + public Task SetCameraAsync(CameraState camera, CancellationToken ct = default) + { + // TODO: Implement MAUI-specific camera handling + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + { + // TODO: Implement MAUI-specific cleanup + return ValueTask.CompletedTask; + } +} From 06d383170e13f71e46108c54a5087f5df005a520 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 11:36:42 +0000 Subject: [PATCH 04/22] Address code review feedback: add IDisposable, fix event leaks, improve constants Co-authored-by: akinbender <40242943+akinbender@users.noreply.github.com> --- .../Components/CadDocumentHost.razor | 10 ++++++- .../Documents/CadabilityDocumentHost.cs | 26 +++++++++++++++++-- .../Rendering/SceneBuilder.cs | 12 +++++++-- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/MakerPrompt.Blazor/Components/CadDocumentHost.razor b/MakerPrompt.Blazor/Components/CadDocumentHost.razor index 73232c5..3f162a8 100644 --- a/MakerPrompt.Blazor/Components/CadDocumentHost.razor +++ b/MakerPrompt.Blazor/Components/CadDocumentHost.razor @@ -135,7 +135,15 @@ private async void OnDocumentChanged(object? sender, EventArgs e) { - await UpdateScene(); + try + { + await UpdateScene(); + } + catch (Exception ex) + { + _errorMessage = $"Error updating scene: {ex.Message}"; + StateHasChanged(); + } } private async Task UpdateScene() diff --git a/MakerPrompt.Shared.ShapeIt/Documents/CadabilityDocumentHost.cs b/MakerPrompt.Shared.ShapeIt/Documents/CadabilityDocumentHost.cs index abda464..fac99a3 100644 --- a/MakerPrompt.Shared.ShapeIt/Documents/CadabilityDocumentHost.cs +++ b/MakerPrompt.Shared.ShapeIt/Documents/CadabilityDocumentHost.cs @@ -9,12 +9,13 @@ namespace MakerPrompt.Shared.ShapeIt.Documents; /// CADability-based implementation of ICadDocumentHost. /// Wraps a CADability Project and Model to provide a UI-agnostic CAD document interface. /// -public class CadabilityDocumentHost : ICadDocumentHost +public class CadabilityDocumentHost : ICadDocumentHost, IDisposable { private Project? _project; private Model? _activeModel; private readonly Guid _id = Guid.NewGuid(); private string? _name; + private bool _disposed; /// public Guid Id => _id; @@ -52,7 +53,6 @@ public async Task LoadAsync(Stream fileStream, string? fileName = null, Cancella await fileStream.CopyToAsync(fs, ct); } - _project = Project.Construct(); _project = Project.ReadFromFile(tempFile); _activeModel = _project.GetActiveModel(); _name = fileName ?? "Document"; @@ -157,4 +157,26 @@ private void WireModelEvents() _activeModel.GeoObjectRemovedEvent += (go) => Changed?.Invoke(this, EventArgs.Empty); _activeModel.GeoObjectDidChangeEvent += (sender, change) => Changed?.Invoke(this, EventArgs.Empty); } + + private void UnwireModelEvents() + { + if (_activeModel == null) + return; + + // Remove event handlers to prevent memory leaks + _activeModel.GeoObjectAddedEvent -= (go) => Changed?.Invoke(this, EventArgs.Empty); + _activeModel.GeoObjectRemovedEvent -= (go) => Changed?.Invoke(this, EventArgs.Empty); + _activeModel.GeoObjectDidChangeEvent -= (sender, change) => Changed?.Invoke(this, EventArgs.Empty); + } + + public void Dispose() + { + if (_disposed) + return; + + UnwireModelEvents(); + _activeModel = null; + _project = null; + _disposed = true; + } } diff --git a/MakerPrompt.Shared.ShapeIt/Rendering/SceneBuilder.cs b/MakerPrompt.Shared.ShapeIt/Rendering/SceneBuilder.cs index e197bc4..4428d46 100644 --- a/MakerPrompt.Shared.ShapeIt/Rendering/SceneBuilder.cs +++ b/MakerPrompt.Shared.ShapeIt/Rendering/SceneBuilder.cs @@ -8,6 +8,12 @@ namespace MakerPrompt.Shared.ShapeIt.Rendering; /// public static class SceneBuilder { + /// + /// Default tessellation tolerance for mesh generation. + /// Lower values produce higher quality meshes but with more triangles. + /// + private const double DefaultTessellationTolerance = 0.01; + /// /// Builds a scene snapshot from a CADability Model. /// @@ -55,7 +61,7 @@ public static SceneSnapshot BuildSceneFromModel(Model model, SceneDetailLevel de int[] triangleIndices; BoundingCube extent; - face.GetTriangulation(0.01, out vertices, out uvVertices, out triangleIndices, out extent); + face.GetTriangulation(DefaultTessellationTolerance, out vertices, out uvVertices, out triangleIndices, out extent); if (vertices == null || triangleIndices == null) continue; @@ -68,7 +74,9 @@ public static SceneSnapshot BuildSceneFromModel(Model model, SceneDetailLevel de positions.Add((float)point.y); positions.Add((float)point.z); - // For now, use face normal for all vertices (could be improved) + // Note: Using face normal at center for all vertices. + // For higher quality rendering, proper per-vertex normals should be computed + // based on adjacent faces or from the UV coordinates of each vertex. try { var normal = face.Surface.GetNormal(new CADability.GeoPoint2D(0.5, 0.5)); From 3ea3eb8590126114c7a83038d6e925a25b817708 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:07:26 +0000 Subject: [PATCH 05/22] Initial plan From d91ee786c01792ed9ebba6df755a0fcdc71c02b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:13:53 +0000 Subject: [PATCH 06/22] Add BrailleRAP core engine with models and services Co-authored-by: akinbender <40242943+akinbender@users.noreply.github.com> --- .../BrailleRAP/Models/BrailleCell.cs | 43 +++++ .../BrailleRAP/Models/BrailleDot.cs | 19 +++ .../BrailleRAP/Models/BraillePageLayout.cs | 29 ++++ .../BrailleRAP/Models/GeomPoint.cs | 7 + .../BrailleRAP/Models/MachineConfig.cs | 43 +++++ .../BrailleRAP/Models/PageConfig.cs | 31 ++++ .../Services/BrailleGCodeGenerator.cs | 114 +++++++++++++ .../BrailleRAP/Services/BraillePaginator.cs | 158 ++++++++++++++++++ .../BrailleRAP/Services/BrailleRAPService.cs | 114 +++++++++++++ .../BrailleRAP/Services/BrailleToGeometry.cs | 137 +++++++++++++++ .../BrailleRAP/Services/BrailleTranslator.cs | 118 +++++++++++++ 11 files changed, 813 insertions(+) create mode 100644 MakerPrompt.Shared/BrailleRAP/Models/BrailleCell.cs create mode 100644 MakerPrompt.Shared/BrailleRAP/Models/BrailleDot.cs create mode 100644 MakerPrompt.Shared/BrailleRAP/Models/BraillePageLayout.cs create mode 100644 MakerPrompt.Shared/BrailleRAP/Models/GeomPoint.cs create mode 100644 MakerPrompt.Shared/BrailleRAP/Models/MachineConfig.cs create mode 100644 MakerPrompt.Shared/BrailleRAP/Models/PageConfig.cs create mode 100644 MakerPrompt.Shared/BrailleRAP/Services/BrailleGCodeGenerator.cs create mode 100644 MakerPrompt.Shared/BrailleRAP/Services/BraillePaginator.cs create mode 100644 MakerPrompt.Shared/BrailleRAP/Services/BrailleRAPService.cs create mode 100644 MakerPrompt.Shared/BrailleRAP/Services/BrailleToGeometry.cs create mode 100644 MakerPrompt.Shared/BrailleRAP/Services/BrailleTranslator.cs diff --git a/MakerPrompt.Shared/BrailleRAP/Models/BrailleCell.cs b/MakerPrompt.Shared/BrailleRAP/Models/BrailleCell.cs new file mode 100644 index 0000000..c8770b4 --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Models/BrailleCell.cs @@ -0,0 +1,43 @@ +namespace MakerPrompt.Shared.BrailleRAP.Models +{ + /// + /// Represents a single Braille cell using Unicode Braille Patterns (U+2800 to U+28FF). + /// + public class BrailleCell + { + /// + /// The Unicode character representing this Braille cell. + /// + public char Character { get; set; } + + /// + /// Creates a Braille cell from a Unicode Braille character. + /// + public BrailleCell(char character) + { + Character = character; + } + + /// + /// Gets the value (0-255) representing the dot pattern. + /// + public int GetValue() + { + return Character - 0x2800; + } + + /// + /// Checks if a specific dot is raised in this cell. + /// + public bool HasDot(BrailleDotPosition position) + { + int value = GetValue(); + return (value & (1 << (int)position)) != 0; + } + + public override string ToString() + { + return Character.ToString(); + } + } +} diff --git a/MakerPrompt.Shared/BrailleRAP/Models/BrailleDot.cs b/MakerPrompt.Shared/BrailleRAP/Models/BrailleDot.cs new file mode 100644 index 0000000..df4011a --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Models/BrailleDot.cs @@ -0,0 +1,19 @@ +namespace MakerPrompt.Shared.BrailleRAP.Models +{ + /// + /// Represents the position of a dot in a Braille cell (0-7 for 8-dot Braille). + /// Standard 6-dot positions: 0-5 + /// Extended 8-dot positions: 0-7 + /// + public enum BrailleDotPosition + { + Dot1 = 0, // Top-left + Dot2 = 1, // Middle-left + Dot3 = 2, // Bottom-left + Dot4 = 3, // Top-right + Dot5 = 4, // Middle-right + Dot6 = 5, // Bottom-right + Dot7 = 6, // Extended bottom-left + Dot8 = 7 // Extended bottom-right + } +} diff --git a/MakerPrompt.Shared/BrailleRAP/Models/BraillePageLayout.cs b/MakerPrompt.Shared/BrailleRAP/Models/BraillePageLayout.cs new file mode 100644 index 0000000..e504b97 --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Models/BraillePageLayout.cs @@ -0,0 +1,29 @@ +namespace MakerPrompt.Shared.BrailleRAP.Models +{ + /// + /// Represents a paginated layout of Braille text. + /// + public class BraillePageLayout + { + /// + /// Pages of Braille lines. + /// Each page contains multiple lines, each line is a string of Braille characters. + /// + public List> Pages { get; set; } = new(); + + /// + /// Gets the total number of pages. + /// + public int PageCount => Pages.Count; + + /// + /// Gets a specific page by index. + /// + public List GetPage(int pageIndex) + { + if (pageIndex < 0 || pageIndex >= Pages.Count) + return new List(); + return Pages[pageIndex]; + } + } +} diff --git a/MakerPrompt.Shared/BrailleRAP/Models/GeomPoint.cs b/MakerPrompt.Shared/BrailleRAP/Models/GeomPoint.cs new file mode 100644 index 0000000..4f6d17d --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Models/GeomPoint.cs @@ -0,0 +1,7 @@ +namespace MakerPrompt.Shared.BrailleRAP.Models +{ + /// + /// Represents a geometric point in 2D space for BrailleRAP positioning. + /// + public record GeomPoint(double X, double Y); +} diff --git a/MakerPrompt.Shared/BrailleRAP/Models/MachineConfig.cs b/MakerPrompt.Shared/BrailleRAP/Models/MachineConfig.cs new file mode 100644 index 0000000..5a53eae --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Models/MachineConfig.cs @@ -0,0 +1,43 @@ +namespace MakerPrompt.Shared.BrailleRAP.Models +{ + /// + /// Configuration for BrailleRAP machine parameters. + /// + public class MachineConfig + { + /// + /// Horizontal spacing between dots in a cell (mm). + /// + public double DotPaddingX { get; set; } = 2.2; + + /// + /// Vertical spacing between dots in a cell (mm). + /// + public double DotPaddingY { get; set; } = 2.2; + + /// + /// Horizontal spacing between cells (mm). + /// + public double CellPaddingX { get; set; } = 6.0; + + /// + /// Vertical spacing between lines (mm). + /// + public double CellPaddingY { get; set; } = 12.0; + + /// + /// Feed rate for movement (mm/min). + /// + public int FeedRate { get; set; } = 6000; + + /// + /// X-axis offset from origin (mm). + /// + public double OffsetX { get; set; } = 0.0; + + /// + /// Y-axis offset from origin (mm). + /// + public double OffsetY { get; set; } = 0.0; + } +} diff --git a/MakerPrompt.Shared/BrailleRAP/Models/PageConfig.cs b/MakerPrompt.Shared/BrailleRAP/Models/PageConfig.cs new file mode 100644 index 0000000..6dec25f --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Models/PageConfig.cs @@ -0,0 +1,31 @@ +namespace MakerPrompt.Shared.BrailleRAP.Models +{ + /// + /// Configuration for Braille page layout. + /// + public class PageConfig + { + /// + /// Number of Braille cells per line. + /// + public int Columns { get; set; } = 28; + + /// + /// Maximum number of lines per page. + /// + public int Rows { get; set; } = 21; + + /// + /// Line spacing multiplier (0 = normal, 1 = double space, etc.). + /// + public int LineSpacing { get; set; } = 0; + + /// + /// Gets the computed number of rows based on spacing. + /// + public int GetComputedRows() + { + return (int)Math.Floor(Rows / ((LineSpacing * 0.5) + 1)); + } + } +} diff --git a/MakerPrompt.Shared/BrailleRAP/Services/BrailleGCodeGenerator.cs b/MakerPrompt.Shared/BrailleRAP/Services/BrailleGCodeGenerator.cs new file mode 100644 index 0000000..d065c20 --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Services/BrailleGCodeGenerator.cs @@ -0,0 +1,114 @@ +using MakerPrompt.Shared.BrailleRAP.Models; +using System.Text; + +namespace MakerPrompt.Shared.BrailleRAP.Services +{ + /// + /// Generates G-code for BrailleRAP embossing from geometric points. + /// Ported from AccessBrailleRAP's GeomToGCode.js + /// + public class BrailleGCodeGenerator + { + private readonly MachineConfig _config; + + public BrailleGCodeGenerator(MachineConfig config) + { + _config = config; + } + + /// + /// Generates complete G-code from geometric points. + /// + public string GenerateGCode(List points) + { + var gcode = new StringBuilder(); + + // Initialize + gcode.Append(Home()); + gcode.Append(SetSpeed(_config.FeedRate)); + gcode.Append(MoveTo(0, 0)); + + // Process each point + foreach (var point in points) + { + gcode.Append(MoveTo(point.X, point.Y)); + gcode.Append(PrintDot()); + } + + // Return to home position + gcode.Append(MoveTo(0, 300)); + gcode.Append(MotorOff()); + + return gcode.ToString(); + } + + /// + /// Generates G-code from a Braille page layout. + /// + public string GenerateGCodeFromLayout(BraillePageLayout layout, int pageIndex = 0) + { + var page = layout.GetPage(pageIndex); + if (page.Count == 0) + return string.Empty; + + var geometry = new BrailleToGeometry(_config); + var points = geometry.BraillePageToGeom(page, _config.OffsetX, _config.OffsetY); + + return GenerateGCode(points); + } + + private string MotorOff() + { + return "M84;\r\n"; + } + + private string Home() + { + var sb = new StringBuilder(); + sb.Append("G28 X;\r\n"); + sb.Append("G28 Y;\r\n"); + return sb.ToString(); + } + + private string GCodePosition(double? x, double? y) + { + if (x == null && y == null) + { + throw new ArgumentException("At least one coordinate must be specified"); + } + + var code = new StringBuilder(); + + if (x.HasValue) + { + code.Append($" X{x.Value:F2}"); + } + + if (y.HasValue) + { + code.Append($" Y{y.Value:F2}"); + } + + code.Append(";\r\n"); + return code.ToString(); + } + + private string SetSpeed(int speed) + { + return $"G1 F{speed};\r\n"; + } + + private string MoveTo(double x, double y) + { + return "G1" + GCodePosition(x, y); + } + + private string PrintDot() + { + var sb = new StringBuilder(); + sb.Append("M3 S1;\r\n"); + sb.Append("M3 S0;\r\n"); + return sb.ToString(); + } + } +} diff --git a/MakerPrompt.Shared/BrailleRAP/Services/BraillePaginator.cs b/MakerPrompt.Shared/BrailleRAP/Services/BraillePaginator.cs new file mode 100644 index 0000000..296d1f7 --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Services/BraillePaginator.cs @@ -0,0 +1,158 @@ +using MakerPrompt.Shared.BrailleRAP.Models; + +namespace MakerPrompt.Shared.BrailleRAP.Services +{ + /// + /// Paginates Braille text into pages based on column and row constraints. + /// Ported from AccessBrailleRAP's BraillePaginator.js + /// + public class BraillePaginator + { + private PageConfig _config; + private List _sourceLines; + private List> _pages; + private List _currentPage; + private int _computedRows; + + public BraillePaginator() + { + _config = new PageConfig(); + _sourceLines = new List(); + _pages = new List>(); + _currentPage = new List(); + _computedRows = _config.GetComputedRows(); + } + + /// + /// Sets the page configuration. + /// + public void SetConfig(PageConfig config) + { + _config = config; + ComputeRows(); + Update(); + } + + /// + /// Sets the source Braille lines to paginate. + /// + public void SetSourceLines(List lines) + { + _sourceLines = lines; + Update(); + } + + /// + /// Gets the paginated layout. + /// + public BraillePageLayout GetLayout() + { + return new BraillePageLayout { Pages = new List>(_pages) }; + } + + private void ComputeRows() + { + _computedRows = _config.GetComputedRows(); + } + + private void AddLine(string line) + { + _currentPage.Add(line); + if (_currentPage.Count >= _computedRows) + { + _pages.Add(new List(_currentPage)); + _currentPage.Clear(); + } + } + + private void FlushLine() + { + if (_currentPage.Count > 0) + { + _pages.Add(new List(_currentPage)); + _currentPage.Clear(); + } + } + + private void Update() + { + if (_sourceLines == null) + return; + + _pages.Clear(); + _currentPage.Clear(); + ComputeRows(); + + foreach (var srcLine in _sourceLines) + { + // Split by blank Braille cells (U+2800 is the blank cell used as space) + var words = srcLine.Split('\u2800'); + + var currentLine = ""; + + foreach (var word in words) + { + // Check for form feed + if (word == "\f") + { + AddLine(currentLine); + currentLine = ""; + FlushLine(); + continue; + } + + // Check if adding this word would exceed column limit + if (word.Length + currentLine.Length >= _config.Columns) + { + if (currentLine.Length > 0) + { + // Add current line and start new one + AddLine(currentLine); + + if (word.Length < _config.Columns) + { + currentLine = word + '\u2800'; + } + else + { + // Word is too long, need to split it + currentLine = ""; + SplitLongWord(word); + } + } + else + { + // Need to split a long word + SplitLongWord(word); + currentLine = ""; + } + } + else + { + currentLine += word; + currentLine += '\u2800'; + } + } + + if (currentLine.Length > 0) + { + AddLine(currentLine); + } + } + + FlushLine(); + } + + private void SplitLongWord(string word) + { + int start = 0; + while (start < word.Length) + { + int chunkSize = Math.Min(_config.Columns, word.Length - start); + string chunk = word.Substring(start, chunkSize); + AddLine(chunk); + start += chunkSize; + } + } + } +} diff --git a/MakerPrompt.Shared/BrailleRAP/Services/BrailleRAPService.cs b/MakerPrompt.Shared/BrailleRAP/Services/BrailleRAPService.cs new file mode 100644 index 0000000..c427f27 --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Services/BrailleRAPService.cs @@ -0,0 +1,114 @@ +using MakerPrompt.Shared.BrailleRAP.Models; + +namespace MakerPrompt.Shared.BrailleRAP.Services +{ + /// + /// Main service for BrailleRAP operations. + /// Coordinates translation, pagination, and G-code generation. + /// + public class BrailleRAPService + { + private readonly BrailleTranslator _translator; + private readonly BraillePaginator _paginator; + private PageConfig _pageConfig; + private MachineConfig _machineConfig; + + public BrailleRAPService() + { + _translator = new BrailleTranslator(); + _paginator = new BraillePaginator(); + _pageConfig = new PageConfig(); + _machineConfig = new MachineConfig(); + } + + /// + /// Sets the page configuration. + /// + public void SetPageConfig(PageConfig config) + { + _pageConfig = config; + _paginator.SetConfig(config); + } + + /// + /// Sets the machine configuration. + /// + public void SetMachineConfig(MachineConfig config) + { + _machineConfig = config; + } + + /// + /// Gets the current page configuration. + /// + public PageConfig GetPageConfig() => _pageConfig; + + /// + /// Gets the current machine configuration. + /// + public MachineConfig GetMachineConfig() => _machineConfig; + + /// + /// Translates text to Braille and returns the layout. + /// + public BraillePageLayout TranslateAndLayout(string text) + { + // Translate text to Braille + var brailleLines = _translator.Translate(text); + + // Paginate + _paginator.SetSourceLines(brailleLines); + + return _paginator.GetLayout(); + } + + /// + /// Generates G-code from text for a specific page. + /// + public string GenerateGCode(string text, int pageIndex = 0) + { + var layout = TranslateAndLayout(text); + + if (layout.PageCount == 0 || pageIndex >= layout.PageCount) + return string.Empty; + + var generator = new BrailleGCodeGenerator(_machineConfig); + return generator.GenerateGCodeFromLayout(layout, pageIndex); + } + + /// + /// Gets a preview of the Braille text for a specific page. + /// + public List GetBraillePreview(string text, int pageIndex = 0) + { + var layout = TranslateAndLayout(text); + + if (layout.PageCount == 0 || pageIndex >= layout.PageCount) + return new List(); + + return layout.GetPage(pageIndex); + } + + /// + /// Gets statistics about the translated and paginated text. + /// + public (int PageCount, int TotalLines, int TotalCharacters) GetStatistics(string text) + { + var layout = TranslateAndLayout(text); + + int totalLines = 0; + int totalChars = 0; + + foreach (var page in layout.Pages) + { + totalLines += page.Count; + foreach (var line in page) + { + totalChars += line.Length; + } + } + + return (layout.PageCount, totalLines, totalChars); + } + } +} diff --git a/MakerPrompt.Shared/BrailleRAP/Services/BrailleToGeometry.cs b/MakerPrompt.Shared/BrailleRAP/Services/BrailleToGeometry.cs new file mode 100644 index 0000000..2e55fa6 --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Services/BrailleToGeometry.cs @@ -0,0 +1,137 @@ +using MakerPrompt.Shared.BrailleRAP.Models; + +namespace MakerPrompt.Shared.BrailleRAP.Services +{ + /// + /// Converts Braille cells to geometric points for embossing. + /// Ported from AccessBrailleRAP's BrailleToGeometry.js + /// + public class BrailleToGeometry + { + // Standard 8-dot Braille dot positions + private static readonly (int X, int Y)[] DotPositions = new[] + { + (0, 0), // Dot 1 + (0, 1), // Dot 2 + (0, 2), // Dot 3 + (1, 0), // Dot 4 + (1, 1), // Dot 5 + (1, 2), // Dot 6 + (0, 3), // Dot 7 + (1, 3) // Dot 8 + }; + + private readonly MachineConfig _config; + + public BrailleToGeometry(MachineConfig config) + { + _config = config; + } + + /// + /// Converts a single Braille character to geometric points. + /// + public List BrailleCharToGeom(char brailleChar, double offsetX, double offsetY) + { + var points = new List(); + int value = brailleChar - 0x2800; + + for (int i = 0; i < 8; i++) + { + if ((value & (1 << i)) != 0) + { + var dot = DotPositions[i]; + var point = new GeomPoint( + dot.X * _config.DotPaddingX + offsetX, + dot.Y * _config.DotPaddingY + offsetY + ); + points.Add(point); + } + } + + return points; + } + + /// + /// Converts a page of Braille text to geometric points. + /// + public List BraillePageToGeom(List lines, double offsetX, double offsetY) + { + var geometry = new List(); + var startY = offsetY; + + foreach (var line in lines) + { + var startX = offsetX; + + foreach (var ch in line) + { + var points = BrailleCharToGeom(ch, startX, startY); + geometry.AddRange(points); + startX += _config.CellPaddingX; + } + + startY += _config.CellPaddingY; + } + + // Sort geometry + SortGeom(geometry); + + // Apply zig-zag optimization for efficient printing + var sorted = SortGeomZigZag(geometry); + + return sorted; + } + + /// + /// Sorts geometry by Y then X coordinates. + /// + private void SortGeom(List geom) + { + geom.Sort((a, b) => + { + if (Math.Abs(a.Y - b.Y) < 0.001) + return a.X.CompareTo(b.X); + return a.Y.CompareTo(b.Y); + }); + } + + /// + /// Sorts geometry in a zig-zag pattern for efficient printing. + /// Alternates direction for each row to minimize travel distance. + /// + private List SortGeomZigZag(List geom) + { + if (geom == null || geom.Count == 0) + return new List(); + + var sorted = new List(); + int start = 0; + int end = 0; + int direction = 1; + + while (end < geom.Count) + { + // Find all points with the same Y coordinate + while (end < geom.Count && Math.Abs(geom[start].Y - geom[end].Y) < 0.001) + { + end++; + } + + // Extract this row + var row = geom.GetRange(start, end - start); + + // Sort by X, alternating direction + row.Sort((a, b) => direction * a.X.CompareTo(b.X)); + + sorted.AddRange(row); + + // Alternate direction for next row + direction = -direction; + start = end; + } + + return sorted; + } + } +} diff --git a/MakerPrompt.Shared/BrailleRAP/Services/BrailleTranslator.cs b/MakerPrompt.Shared/BrailleRAP/Services/BrailleTranslator.cs new file mode 100644 index 0000000..8175af5 --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Services/BrailleTranslator.cs @@ -0,0 +1,118 @@ +using MakerPrompt.Shared.BrailleRAP.Models; + +namespace MakerPrompt.Shared.BrailleRAP.Services +{ + /// + /// Translates text to Braille using simple character mapping. + /// This is a basic implementation supporting English Grade 1 Braille. + /// + public class BrailleTranslator + { + // Basic English Grade 1 Braille mapping (simplified) + // Unicode Braille patterns start at U+2800 + private static readonly Dictionary EnglishGrade1Map = new() + { + // Letters (a-z) + { 'a', '\u2801' }, { 'b', '\u2803' }, { 'c', '\u2809' }, { 'd', '\u2819' }, + { 'e', '\u2811' }, { 'f', '\u280B' }, { 'g', '\u281B' }, { 'h', '\u2813' }, + { 'i', '\u280A' }, { 'j', '\u281A' }, { 'k', '\u2805' }, { 'l', '\u2807' }, + { 'm', '\u280D' }, { 'n', '\u281D' }, { 'o', '\u2815' }, { 'p', '\u280F' }, + { 'q', '\u281F' }, { 'r', '\u2817' }, { 's', '\u280E' }, { 't', '\u281E' }, + { 'u', '\u2825' }, { 'v', '\u2827' }, { 'w', '\u283A' }, { 'x', '\u282D' }, + { 'y', '\u283D' }, { 'z', '\u2835' }, + + // Capital letter indicator + { 'A', '\u2820' }, { 'B', '\u2820' }, { 'C', '\u2820' }, { 'D', '\u2820' }, + { 'E', '\u2820' }, { 'F', '\u2820' }, { 'G', '\u2820' }, { 'H', '\u2820' }, + { 'I', '\u2820' }, { 'J', '\u2820' }, { 'K', '\u2820' }, { 'L', '\u2820' }, + { 'M', '\u2820' }, { 'N', '\u2820' }, { 'O', '\u2820' }, { 'P', '\u2820' }, + { 'Q', '\u2820' }, { 'R', '\u2820' }, { 'S', '\u2820' }, { 'T', '\u2820' }, + { 'U', '\u2820' }, { 'V', '\u2820' }, { 'W', '\u2820' }, { 'X', '\u2820' }, + { 'Y', '\u2820' }, { 'Z', '\u2820' }, + + // Numbers + { '1', '\u2801' }, { '2', '\u2803' }, { '3', '\u2809' }, { '4', '\u2819' }, + { '5', '\u2811' }, { '6', '\u280B' }, { '7', '\u281B' }, { '8', '\u2813' }, + { '9', '\u280A' }, { '0', '\u281A' }, + + // Common punctuation + { ' ', '\u2800' }, // Blank cell for space + { ',', '\u2802' }, + { '.', '\u2832' }, + { '?', '\u2826' }, + { '!', '\u2816' }, + { '\'', '\u2804' }, + { '-', '\u2824' }, + { ':', '\u2812' }, + { ';', '\u2822' }, + { '(', '\u2823' }, + { ')', '\u281C' }, + { '\n', '\n' }, // Preserve newlines + { '\f', '\f' }, // Preserve form feeds + }; + + /// + /// Translates plain text to Braille. + /// + /// The text to translate. + /// List of Braille lines. + public List Translate(string text) + { + if (string.IsNullOrEmpty(text)) + return new List(); + + // Split by newlines but preserve form feeds + var lines = text.Split(new[] { '\r', '\n' }, StringSplitOptions.None) + .Where(line => !string.IsNullOrEmpty(line) || line == string.Empty) + .ToList(); + + var brailleLines = new List(); + + foreach (var line in lines) + { + if (line.Contains('\f')) + { + brailleLines.Add("\f"); + continue; + } + + var brailleLine = new System.Text.StringBuilder(); + bool nextIsCapital = false; + + foreach (var ch in line) + { + if (char.IsUpper(ch)) + { + // Add capital indicator before the letter + brailleLine.Append('\u2820'); + // Then add the lowercase version + var lower = char.ToLower(ch); + if (EnglishGrade1Map.TryGetValue(lower, out var brailleChar)) + { + brailleLine.Append(brailleChar); + } + else + { + // Unknown character, use blank + brailleLine.Append('\u2800'); + } + } + else if (EnglishGrade1Map.TryGetValue(ch, out var brailleChar)) + { + if (brailleChar != '\n' && brailleChar != '\f') + brailleLine.Append(brailleChar); + } + else + { + // Unknown character, use blank cell + brailleLine.Append('\u2800'); + } + } + + brailleLines.Add(brailleLine.ToString()); + } + + return brailleLines; + } + } +} From 54cbb05109d64b05f367857047e233e213d6f891 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:19:06 +0000 Subject: [PATCH 07/22] Add BrailleRAP UI page with navigation and localization Co-authored-by: akinbender <40242943+akinbender@users.noreply.github.com> --- MakerPrompt.Shared/Layout/NavMenu.razor | 5 + MakerPrompt.Shared/Pages/BrailleRAPPage.razor | 366 ++ .../Properties/Resources.Designer.cs | 3133 +++++++++-------- MakerPrompt.Shared/Properties/Resources.resx | 90 + 4 files changed, 2163 insertions(+), 1431 deletions(-) create mode 100644 MakerPrompt.Shared/Pages/BrailleRAPPage.razor diff --git a/MakerPrompt.Shared/Layout/NavMenu.razor b/MakerPrompt.Shared/Layout/NavMenu.razor index f716897..62b65a4 100644 --- a/MakerPrompt.Shared/Layout/NavMenu.razor +++ b/MakerPrompt.Shared/Layout/NavMenu.razor @@ -20,6 +20,11 @@ @Resources.PageTitle_Calculators + + +