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..b4e02c8 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,10 @@ builder.RootComponents.Add("head::after"); 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; + } +} diff --git a/MakerPrompt.Shared.ShapeIt/Documents/CadabilityDocumentHost.cs b/MakerPrompt.Shared.ShapeIt/Documents/CadabilityDocumentHost.cs new file mode 100644 index 0000000..a808e06 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Documents/CadabilityDocumentHost.cs @@ -0,0 +1,264 @@ +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, IDisposable +{ + private Project? _project; + private Model? _activeModel; + private readonly Guid _id = Guid.NewGuid(); + private string? _name; + private bool _disposed; + + /// + 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.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); + } + + /// + public Task CreateBoxAsync(double centerX, double centerY, double centerZ, + double width, double depth, double height, + CancellationToken ct = default) + { + if (_activeModel == null) + throw new InvalidOperationException("No model is loaded."); + + // Create a box using CADability Make3D + var origin = new CADability.GeoPoint(centerX - width/2, centerY - depth/2, centerZ); + var xLength = width * CADability.GeoVector.XAxis; + var yLength = depth * CADability.GeoVector.YAxis; + var zLength = height * CADability.GeoVector.ZAxis; + + var box = CADability.GeoObject.Make3D.MakeBox(origin, xLength, yLength, zLength); + + _activeModel.Add(box); + Changed?.Invoke(this, EventArgs.Empty); + + return Task.CompletedTask; + } + + /// + public Task CreateSphereAsync(double centerX, double centerY, double centerZ, + double radius, + CancellationToken ct = default) + { + if (_activeModel == null) + throw new InvalidOperationException("No model is loaded."); + + // Create a sphere using CADability Make3D + var center = new CADability.GeoPoint(centerX, centerY, centerZ); + var sphere = CADability.GeoObject.Make3D.MakeSphere(center, radius); + + _activeModel.Add(sphere); + Changed?.Invoke(this, EventArgs.Empty); + + return Task.CompletedTask; + } + + /// + public Task CreateCylinderAsync(double baseX, double baseY, double baseZ, + double radius, double height, + CancellationToken ct = default) + { + if (_activeModel == null) + throw new InvalidOperationException("No model is loaded."); + + // Create a cylinder using CADability Make3D + var baseCenter = new CADability.GeoPoint(baseX, baseY, baseZ); + var axis = height * CADability.GeoVector.ZAxis; + var radiusVec = radius * CADability.GeoVector.XAxis; + + var cylinder = CADability.GeoObject.Make3D.MakeCylinder(baseCenter, axis, radiusVec); + + _activeModel.Add(cylinder); + Changed?.Invoke(this, EventArgs.Empty); + + return Task.CompletedTask; + } + + /// + public Task CreateConeAsync(double baseX, double baseY, double baseZ, + double baseRadius, double height, double topRadius = 0, + CancellationToken ct = default) + { + if (_activeModel == null) + throw new InvalidOperationException("No model is loaded."); + + // Create a cone using CADability Make3D + var baseCenter = new CADability.GeoPoint(baseX, baseY, baseZ); + var axis = height * CADability.GeoVector.ZAxis; + var baseRadiusVec = baseRadius * CADability.GeoVector.XAxis; + + var cone = CADability.GeoObject.Make3D.MakeCone(baseCenter, axis, baseRadiusVec, baseRadius, topRadius); + + _activeModel.Add(cone); + Changed?.Invoke(this, EventArgs.Empty); + + return Task.CompletedTask; + } + + 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); + } + + 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/Documents/ICadDocumentHost.cs b/MakerPrompt.Shared.ShapeIt/Documents/ICadDocumentHost.cs new file mode 100644 index 0000000..e4d90d9 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Documents/ICadDocumentHost.cs @@ -0,0 +1,132 @@ +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); + + /// + /// Creates a box (rectangular prism) solid and adds it to the document. + /// + /// Center X coordinate. + /// Center Y coordinate. + /// Center Z coordinate. + /// Width (X direction). + /// Depth (Y direction). + /// Height (Z direction). + /// Cancellation token. + Task CreateBoxAsync(double centerX, double centerY, double centerZ, + double width, double depth, double height, + CancellationToken ct = default); + + /// + /// Creates a sphere solid and adds it to the document. + /// + /// Center X coordinate. + /// Center Y coordinate. + /// Center Z coordinate. + /// Sphere radius. + /// Cancellation token. + Task CreateSphereAsync(double centerX, double centerY, double centerZ, + double radius, + CancellationToken ct = default); + + /// + /// Creates a cylinder solid and adds it to the document. + /// + /// Base center X coordinate. + /// Base center Y coordinate. + /// Base center Z coordinate. + /// Cylinder radius. + /// Cylinder height (Z direction). + /// Cancellation token. + Task CreateCylinderAsync(double baseX, double baseY, double baseZ, + double radius, double height, + CancellationToken ct = default); + + /// + /// Creates a cone solid and adds it to the document. + /// + /// Base center X coordinate. + /// Base center Y coordinate. + /// Base center Z coordinate. + /// Base radius. + /// Cone height (Z direction). + /// Top radius (0 for sharp cone). + /// Cancellation token. + Task CreateConeAsync(double baseX, double baseY, double baseZ, + double baseRadius, double height, double topRadius = 0, + 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..4428d46 --- /dev/null +++ b/MakerPrompt.Shared.ShapeIt/Rendering/SceneBuilder.cs @@ -0,0 +1,147 @@ +using CADability; +using CADability.GeoObject; + +namespace MakerPrompt.Shared.ShapeIt.Rendering; + +/// +/// Builds scene snapshots from CADability Model objects. +/// +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. + /// + 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(DefaultTessellationTolerance, 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); + + // 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)); + 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.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..9852b6c --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Models/MachineConfig.cs @@ -0,0 +1,48 @@ +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; + + /// + /// Y-coordinate for return position after printing (mm). + /// + public double ReturnPositionY { get; set; } = 300.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..95d2aee --- /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, _config.ReturnPositionY)); + 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..8b6afb7 --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Services/BraillePaginator.cs @@ -0,0 +1,160 @@ +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 const char BlankBrailleCell = '\u2800'; + + 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(BlankBrailleCell); + + 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 + BlankBrailleCell; + } + 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 += BlankBrailleCell; + } + } + + 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..0ec6b28 --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Services/BrailleRAPService.cs @@ -0,0 +1,130 @@ +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; + private BrailleLanguage _currentLanguage; + + public BrailleRAPService() + { + _translator = new BrailleTranslator(); + _paginator = new BraillePaginator(); + _pageConfig = new PageConfig(); + _machineConfig = new MachineConfig(); + _currentLanguage = BrailleLanguage.EnglishGrade1; + } + + /// + /// Sets the Braille translation language. + /// + public void SetLanguage(BrailleLanguage language) + { + _currentLanguage = language; + _translator.SetLanguage(language); + } + + /// + /// Gets the current Braille language. + /// + public BrailleLanguage GetLanguage() => _currentLanguage; + + /// + /// 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..25fd3b1 --- /dev/null +++ b/MakerPrompt.Shared/BrailleRAP/Services/BrailleTranslator.cs @@ -0,0 +1,308 @@ +using MakerPrompt.Shared.BrailleRAP.Models; + +namespace MakerPrompt.Shared.BrailleRAP.Services +{ + /// + /// Supported Braille languages and translation tables. + /// + public enum BrailleLanguage + { + EnglishGrade1, + FrenchGrade1, + GermanGrade1, + SpanishGrade1 + } + + /// + /// Translates text to Braille using character mapping for multiple languages. + /// + public class BrailleTranslator + { + // Braille character constants + private const char CapitalIndicator = '\u2820'; + private const char BlankCell = '\u2800'; + private const char NumberIndicator = '\u283C'; + + private BrailleLanguage _currentLanguage = BrailleLanguage.EnglishGrade1; + + /// + /// Sets the translation language. + /// + public void SetLanguage(BrailleLanguage language) + { + _currentLanguage = language; + } + + /// + /// Gets the current language. + /// + public BrailleLanguage GetLanguage() => _currentLanguage; + + // 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', CapitalIndicator }, { 'B', CapitalIndicator }, { 'C', CapitalIndicator }, { 'D', CapitalIndicator }, + { 'E', CapitalIndicator }, { 'F', CapitalIndicator }, { 'G', CapitalIndicator }, { 'H', CapitalIndicator }, + { 'I', CapitalIndicator }, { 'J', CapitalIndicator }, { 'K', CapitalIndicator }, { 'L', CapitalIndicator }, + { 'M', CapitalIndicator }, { 'N', CapitalIndicator }, { 'O', CapitalIndicator }, { 'P', CapitalIndicator }, + { 'Q', CapitalIndicator }, { 'R', CapitalIndicator }, { 'S', CapitalIndicator }, { 'T', CapitalIndicator }, + { 'U', CapitalIndicator }, { 'V', CapitalIndicator }, { 'W', CapitalIndicator }, { 'X', CapitalIndicator }, + { 'Y', CapitalIndicator }, { 'Z', CapitalIndicator }, + + // Numbers + { '1', '\u2801' }, { '2', '\u2803' }, { '3', '\u2809' }, { '4', '\u2819' }, + { '5', '\u2811' }, { '6', '\u280B' }, { '7', '\u281B' }, { '8', '\u2813' }, + { '9', '\u280A' }, { '0', '\u281A' }, + + // Common punctuation + { ' ', BlankCell }, // Blank cell for space + { ',', '\u2802' }, + { '.', '\u2832' }, + { '?', '\u2826' }, + { '!', '\u2816' }, + { '\'', '\u2804' }, + { '-', '\u2824' }, + { ':', '\u2812' }, + { ';', '\u2822' }, + { '(', '\u2823' }, + { ')', '\u281C' }, + { '\n', '\n' }, // Preserve newlines + { '\f', '\f' }, // Preserve form feeds + }; + + // French Grade 1 Braille mapping + private static readonly Dictionary FrenchGrade1Map = new() + { + // Letters (a-z) - same as English + { '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 letters + { 'A', CapitalIndicator }, { 'B', CapitalIndicator }, { 'C', CapitalIndicator }, { 'D', CapitalIndicator }, + { 'E', CapitalIndicator }, { 'F', CapitalIndicator }, { 'G', CapitalIndicator }, { 'H', CapitalIndicator }, + { 'I', CapitalIndicator }, { 'J', CapitalIndicator }, { 'K', CapitalIndicator }, { 'L', CapitalIndicator }, + { 'M', CapitalIndicator }, { 'N', CapitalIndicator }, { 'O', CapitalIndicator }, { 'P', CapitalIndicator }, + { 'Q', CapitalIndicator }, { 'R', CapitalIndicator }, { 'S', CapitalIndicator }, { 'T', CapitalIndicator }, + { 'U', CapitalIndicator }, { 'V', CapitalIndicator }, { 'W', CapitalIndicator }, { 'X', CapitalIndicator }, + { 'Y', CapitalIndicator }, { 'Z', CapitalIndicator }, + + // Numbers + { '1', '\u2801' }, { '2', '\u2803' }, { '3', '\u2809' }, { '4', '\u2819' }, + { '5', '\u2811' }, { '6', '\u280B' }, { '7', '\u281B' }, { '8', '\u2813' }, + { '9', '\u280A' }, { '0', '\u281A' }, + + // French accented characters + { 'à', '\u282F' }, { 'â', '\u2801' }, { 'ç', '\u280F' }, + { 'é', '\u283F' }, { 'è', '\u282E' }, { 'ê', '\u2811' }, + { 'ë', '\u283B' }, { 'î', '\u280A' }, { 'ï', '\u283B' }, + { 'ô', '\u2815' }, { 'ù', '\u283D' }, { 'û', '\u2825' }, + { 'ü', '\u283B' }, { 'œ', '\u283B' }, + + // Common punctuation + { ' ', BlankCell }, + { ',', '\u2802' }, + { '.', '\u2832' }, + { '?', '\u2826' }, + { '!', '\u2816' }, + { '\'', '\u2804' }, + { '-', '\u2824' }, + { ':', '\u2812' }, + { ';', '\u2822' }, + { '(', '\u2823' }, + { ')', '\u281C' }, + { '\n', '\n' }, + { '\f', '\f' }, + }; + + // German Grade 1 Braille mapping + private static readonly Dictionary GermanGrade1Map = new() + { + // Letters (a-z) - same as English + { '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 letters + { 'A', CapitalIndicator }, { 'B', CapitalIndicator }, { 'C', CapitalIndicator }, { 'D', CapitalIndicator }, + { 'E', CapitalIndicator }, { 'F', CapitalIndicator }, { 'G', CapitalIndicator }, { 'H', CapitalIndicator }, + { 'I', CapitalIndicator }, { 'J', CapitalIndicator }, { 'K', CapitalIndicator }, { 'L', CapitalIndicator }, + { 'M', CapitalIndicator }, { 'N', CapitalIndicator }, { 'O', CapitalIndicator }, { 'P', CapitalIndicator }, + { 'Q', CapitalIndicator }, { 'R', CapitalIndicator }, { 'S', CapitalIndicator }, { 'T', CapitalIndicator }, + { 'U', CapitalIndicator }, { 'V', CapitalIndicator }, { 'W', CapitalIndicator }, { 'X', CapitalIndicator }, + { 'Y', CapitalIndicator }, { 'Z', CapitalIndicator }, + + // Numbers + { '1', '\u2801' }, { '2', '\u2803' }, { '3', '\u2809' }, { '4', '\u2819' }, + { '5', '\u2811' }, { '6', '\u280B' }, { '7', '\u281B' }, { '8', '\u2813' }, + { '9', '\u280A' }, { '0', '\u281A' }, + + // German umlauts and special characters + { 'ä', '\u2831' }, { 'Ä', '\u2831' }, + { 'ö', '\u283B' }, { 'Ö', '\u283B' }, + { 'ü', '\u283C' }, { 'Ü', '\u283C' }, + { 'ß', '\u282E' }, + + // Common punctuation + { ' ', BlankCell }, + { ',', '\u2802' }, + { '.', '\u2832' }, + { '?', '\u2826' }, + { '!', '\u2816' }, + { '\'', '\u2804' }, + { '-', '\u2824' }, + { ':', '\u2812' }, + { ';', '\u2822' }, + { '(', '\u2823' }, + { ')', '\u281C' }, + { '\n', '\n' }, + { '\f', '\f' }, + }; + + // Spanish Grade 1 Braille mapping + private static readonly Dictionary SpanishGrade1Map = new() + { + // Letters (a-z) - same as English + { '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 letters + { 'A', CapitalIndicator }, { 'B', CapitalIndicator }, { 'C', CapitalIndicator }, { 'D', CapitalIndicator }, + { 'E', CapitalIndicator }, { 'F', CapitalIndicator }, { 'G', CapitalIndicator }, { 'H', CapitalIndicator }, + { 'I', CapitalIndicator }, { 'J', CapitalIndicator }, { 'K', CapitalIndicator }, { 'L', CapitalIndicator }, + { 'M', CapitalIndicator }, { 'N', CapitalIndicator }, { 'O', CapitalIndicator }, { 'P', CapitalIndicator }, + { 'Q', CapitalIndicator }, { 'R', CapitalIndicator }, { 'S', CapitalIndicator }, { 'T', CapitalIndicator }, + { 'U', CapitalIndicator }, { 'V', CapitalIndicator }, { 'W', CapitalIndicator }, { 'X', CapitalIndicator }, + { 'Y', CapitalIndicator }, { 'Z', CapitalIndicator }, + + // Numbers + { '1', '\u2801' }, { '2', '\u2803' }, { '3', '\u2809' }, { '4', '\u2819' }, + { '5', '\u2811' }, { '6', '\u280B' }, { '7', '\u281B' }, { '8', '\u2813' }, + { '9', '\u280A' }, { '0', '\u281A' }, + + // Spanish accented characters + { 'á', '\u2831' }, { 'é', '\u283F' }, { 'í', '\u280C' }, + { 'ó', '\u283B' }, { 'ú', '\u283C' }, + { 'ñ', '\u283B' }, { 'ü', '\u283C' }, + { '¿', '\u2826' }, { '¡', '\u2816' }, + + // Common punctuation + { ' ', BlankCell }, + { ',', '\u2802' }, + { '.', '\u2832' }, + { '?', '\u2826' }, + { '!', '\u2816' }, + { '\'', '\u2804' }, + { '-', '\u2824' }, + { ':', '\u2812' }, + { ';', '\u2822' }, + { '(', '\u2823' }, + { ')', '\u281C' }, + { '\n', '\n' }, + { '\f', '\f' }, + }; + + /// + /// Gets the appropriate translation map for the current language. + /// + private Dictionary GetTranslationMap() + { + return _currentLanguage switch + { + BrailleLanguage.FrenchGrade1 => FrenchGrade1Map, + BrailleLanguage.GermanGrade1 => GermanGrade1Map, + BrailleLanguage.SpanishGrade1 => SpanishGrade1Map, + _ => EnglishGrade1Map + }; + } + + /// + /// 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(); + + var translationMap = GetTranslationMap(); + + // 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(); + + foreach (var ch in line) + { + if (char.IsUpper(ch)) + { + // Add capital indicator before the letter + brailleLine.Append(CapitalIndicator); + // Then add the lowercase version + var lower = char.ToLower(ch); + if (translationMap.TryGetValue(lower, out var brailleChar)) + { + brailleLine.Append(brailleChar); + } + else + { + // Unknown character, use blank + brailleLine.Append(BlankCell); + } + } + else if (translationMap.TryGetValue(ch, out var brailleChar)) + { + if (brailleChar != '\n' && brailleChar != '\f') + brailleLine.Append(brailleChar); + } + else + { + // Unknown character, use blank cell + brailleLine.Append(BlankCell); + } + } + + brailleLines.Add(brailleLine.ToString()); + } + + return brailleLines; + } + } +} diff --git a/MakerPrompt.Shared/Components/CadDocumentHost.razor b/MakerPrompt.Shared/Components/CadDocumentHost.razor new file mode 100644 index 0000000..ce0ce93 --- /dev/null +++ b/MakerPrompt.Shared/Components/CadDocumentHost.razor @@ -0,0 +1,293 @@ +@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; + + // Initialize a new document automatically + await DocumentHost.InitializeNewAsync(); + } + 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 Task CreateBox() + { + try + { + _isLoading = true; + _errorMessage = null; + StateHasChanged(); + + // Create a box at origin with default size (20x20x20) + await DocumentHost.CreateBoxAsync(0, 0, 0, 20, 20, 20); + await UpdateScene(); + } + catch (Exception ex) + { + _errorMessage = $"Error creating box: {ex.Message}"; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private async Task CreateSphere() + { + try + { + _isLoading = true; + _errorMessage = null; + StateHasChanged(); + + // Create a sphere at origin with default radius (10) + await DocumentHost.CreateSphereAsync(0, 0, 10, 10); + await UpdateScene(); + } + catch (Exception ex) + { + _errorMessage = $"Error creating sphere: {ex.Message}"; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private async Task CreateCylinder() + { + try + { + _isLoading = true; + _errorMessage = null; + StateHasChanged(); + + // Create a cylinder at origin with default size (radius 10, height 20) + await DocumentHost.CreateCylinderAsync(0, 0, 0, 10, 20); + await UpdateScene(); + } + catch (Exception ex) + { + _errorMessage = $"Error creating cylinder: {ex.Message}"; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private async Task CreateCone() + { + try + { + _isLoading = true; + _errorMessage = null; + StateHasChanged(); + + // Create a cone at origin with default size (base radius 10, height 20, sharp tip) + await DocumentHost.CreateConeAsync(0, 0, 0, 10, 20, 0); + await UpdateScene(); + } + catch (Exception ex) + { + _errorMessage = $"Error creating cone: {ex.Message}"; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private async void OnDocumentChanged(object? sender, EventArgs e) + { + try + { + await UpdateScene(); + } + catch (Exception ex) + { + _errorMessage = $"Error updating scene: {ex.Message}"; + StateHasChanged(); + } + } + + 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.Shared/Layout/NavMenu.razor b/MakerPrompt.Shared/Layout/NavMenu.razor index f716897..a0f1abb 100644 --- a/MakerPrompt.Shared/Layout/NavMenu.razor +++ b/MakerPrompt.Shared/Layout/NavMenu.razor @@ -20,6 +20,16 @@ @Resources.PageTitle_Calculators + +