PolyPlot - Software Interface
Overview
PolyPlot is a web-based interface developed in React.js (Vite) to control a custom-built plotter machine. The interface provides a WYSIWYG canvas editor for creating vector paths, which are then converted into G-code and sent to the machine running FluidNC firmware.
Goals
- Make plotting accessible for students and hobbyists.
- Provide an interactive editor with drawing, editing, and color management tools.
- Ensure compatibility with FluidNC for reliable machine control.
- Optimize workflow: design → G-code → send to machine, all within one interface.
- Provide a modern, modular codebase that can be extended for future fabrication projects.
Why Fabric.js?
The choice of Fabric.js was fundamental to PolyPlot’s design.
We required a library that could:
- Serve as the foundation for a complete canvas editor — not just free drawing, but also shapes, text, groups, alignment, snapping, rulers, and object manipulation.
- Support exporting artwork to SVG reliably, because G-code generation requires clean vector paths.
- Handle serialization (
toJSON
) so we could implement undo/redo and workspace persistence. - Be extensible for custom features like eraser brushes, object metadata, and path-splitting.
Fabric.js was selected over other options (Konva.js, Paper.js, etc.) because:
- Konva.js focuses on performant rendering but lacks built-in path serialization and CAD-like editing.
- Paper.js excels at path operations but is less suited for UI-heavy editors.
- Fabric.js provided the best balance of editing features, extensibility, and SVG support, which directly aligned with PolyPlot’s requirements for G-code workflows.
In short:
👉 Without Fabric.js, PolyPlot would require building an entire editor and SVG exporter from scratch.
Architecture
Core Stack
- React (Vite) → SPA framework.
- Fabric.js v6.4.3 → Interactive canvas & SVG handling.
- FluidNC → Firmware backend (WebSocket & WebServer communication).
- TailwindCSS + Framer Motion → Styling + animations.
- Dynamic Imports (svg-to-gcode) → On-demand G-code conversion.
Contexts
- CanvasContext → Manages Fabric.js state, undo/redo stack, workspace tools.
- ComContext → Handles WebSocket connection to FluidNC, job upload, framing, status updates.
- CustomContext → Custom Context Selector.
State Management in PolyPlot (CustomContext)
Building PolyPlot as a real-time plotting interface was not just about UI — it required tight state synchronization between:
- The plotter hardware (via FluidNC)
- The canvas editor (via Fabric.js)
- The UI controls (progress, job state, color management, etc.)
This meant our state system had to satisfy a few critical needs:
Reactive Updates Without Over-Rendering
The plotter continuously streams machine position updates (e.g., X/Y/Z head positions). If every update triggered a global React re-render, the UI would quickly become sluggish.
Redux-like Granularity Without Redux Overhead
Redux is powerful but heavy-handed for our use case. We wanted the same fine-grained state subscription (update only what’s necessary) but without boilerplate.
Multiple State Domains
Machine state, toolhead state, color state, and configuration all evolve independently but need to remain synchronized in some scenarios.
Why We Introduced createCustomContext
React’s built-in createContext
works well for global state but has a big drawback:
This becomes disastrous when:
- The toolhead position updates multiple times per second.
- Progress counters are incremented every second.
- Users are interacting with Fabric.js objects on the canvas.
So we built a custom createCustomContext
hook (inspired by use-context-selector
).
What it Solves
- Components can subscribe only to the slice of state they care about.
- Prevents unnecessary re-renders across the entire app.
- Gives Redux-like selector behaviour but using only native React Context.
For example:
- The progress bar listens only to
machineState.progress
. - The position dot on the canvas listens only to
toolHeadState.headPos
. - The pen configuration panel listens only to
colorState.colors
.
This way, if the toolhead moves, the progress bar does not re-render, and vice versa.
⚡ Advantages of This Design
- Performance Optimized
- Critical for continuous position updates without dropping frames.
- Keeps canvas rendering smooth while still syncing with hardware.
- Scalable
- Easy to add new state domains (e.g., laser mode, CNC spindle control) without refactoring everything.
- Lightweight Alternative to Redux
- No boilerplate, reducers remain simple, and React DevTools is enough for debugging.
- Context slices mirror hardware/software separation:
MachineStateContext
→ plotter + jobToolHeadStateContext
→ XY/Z head positionColorStateContext
→ multi-pen configsConfigStateContext
→ general settings
- Direct Ref Integration
- Example:
dotRef
(canvas object) updates immediately on toolhead movement without forcing re-renders in React tree. - This was crucial since Fabric.js runs its own rendering cycle independent of React.
- Example:
During early builds, we made the mistake of storing everything in one context. Which resulted in tiny movement of the toolhead causing the entire components that uses the context value to re-render, making the UI laggy. Also leads to the toolhead object being behind in the canvas comparing to the current position of the toolhead while running big drawings. Switching to the customContext
pattern with per-slice providers solved this.
In short we engineered a Redux-like context system to balance real-time reactivity with rendering efficiency. This gave us the performance of a low-level system while staying inside the React ecosystem
Key Features
Editor
- Free drawing + shapes.
- Object manipulation (move, scale, rotate).
- Color management (stroke-based).
- Object alignment & numeric geometry controls.
- Rulers & snapping grid.
- Undo/redo stack (15 states max).
- Save/load state to local storage.
Plotter
- Real-time WebSocket connection to FluidNC.
- G-code conversion (
svg-to-gcode
, dynamically imported). - Job upload and execution.
- Pause/resume job control.
- Tool head position preview (mini-screen).
- Color-based job sorting.
- Safety checks: framing before plot, connection recovery modals.
Key Challenges & Solutions
Undo/Redo
Problem: We initially thought implementing undo/redo would be simple, just call canvas.toJSON()
on every change and push it into a stack. But this approach quickly backfired.
- Every object move, resize, or free-draw stroke triggered a full save.
- JSON snapshots grew larger as the canvas filled with paths.
- The UI became laggy — undoing a single stroke sometimes took over a second.
Solution: We redesigned the system around capped, smarter state saves.
- Only the last 15 states are stored (prevents memory bloat).
- We save only after meaningful events (
object:modified
instead of every mouse move). - A
saveState()
function manages bothundoStack
andredoStack
, clearing redo when a new action occurs.
Outcome: Undo/Redo became instantaneous and predictable, even with large canvases. The capped history gives a safety net without eating memory.
Path Splitting
When we started working with imported SVGs, we quickly ran into the need for path splitting. Many designs came in as large compound paths, but for plotting (and later, G-code generation for multi color), we needed each sub-path to be independent.
The Problem:
- Fabric.js represents an SVG
<path>
as one long command string (d
). - We wanted to break these into smaller, manageable segments so each stroke could be planned separately.
- However, Fabric.js anchors paths based on their bounding box, not absolute coordinates.
- This meant we couldn’t just reset everything to
(0,0)
without distorting positions.
The Solution:
We implemented a relative-coordinate-preserving split function:
- Extract commands from the original
d
attribute. - Build separate Fabric.Path objects, each containing one segment.
- Apply offsets to ensure each new path aligns with the original canvas position.
This worked well enough to let us treat compound paths as multiple objects.
The Remaining Bug:
There’s still one unresolved issue:
- When splitting paths, a small displacement occurs in some cases.
- The new paths don’t perfectly align with the original, especially noticeable on shapes imported from external design tools.
- My suspicion is that it comes from Fabric.js handling of
left/top
vs internalpathOffset
.
Current Workaround:
- After splitting, we manually adjust the positions of misaligned parts.
- This is not ideal, but acceptable for most simple designs.
G-code Path Planning
The Problem:
When converting SVG/paths into G-code, we originally sent them in the order Fabric.js stored them. This created chaotic travel moves: the pen would jump across the canvas randomly, wasting time.
The Solution:
We implemented a nearest-neighbor optimization:
- Start from the last endpoint of the previous path.
- Choose the next closest path by distance.
- Repeat until all paths are covered.
Outcome:
- Plotting time dropped significantly.
- The pen moved more naturally and predictably.
This doesn’t guarantee mathematically optimal ordering (like full TSP solving), but it drastically reduced unnecessary travel.
FluidNC Integration
Integrating PolyPlot with FluidNC was one of the most critical milestones. Unlike typical GRBL setups where streaming G-code line-by-line over a serial connection is common, FluidNC comes with its own WebServer API and onboard file system. Leveraging this properly completely changed how we structured communication.
Problem: How to Send G-code Reliably
Our requirement was simple in theory: the editor should take SVGs, convert them to G-code, and run them on the plotter. But the challenge was how to send thousands of G-code lines reliably to FluidNC.
- Sending G-code line-by-line through the FluidNC webserver API seemed possible at first.
- But this approach was too slow and introduced unnecessary complexity: every API call would need confirmation, and network interruptions could break the job.
Solution: File-Based Workflow
We embraced FluidNC’s built-in webserver file system instead:
- Generate G-code file
- PolyPlot generates the complete G-code for the drawing in one go.
- Upload to FluidNC
- The full file is uploaded to the controller’s internal file system via the webserver API.
- Trigger execution
- Once stored, a command is sent to start the job directly from that file.
This approach let FluidNC handle buffering internally, removing all risks of command loss.
Benefits:
- Reliability: No dropped commands, even for long jobs.
- Speed: One upload, then FluidNC streams internally at full controller speed.
- Resilience: The job continues even if the browser disconnects.
- Simplicity: We avoid the complexity of incremental API calls.
Lessons Learned
- Why Fabric.js? → Because it already had most of the CAD-like editing tools, plus serialization.
- Undo/Redo must be limited to avoid memory issues.
- Workspace persistence is essential for user trust.
- FluidNC requires careful communication handling (throttling + queuing).
- Dynamic imports help keep bundle size small (important for web apps).
Roadmap
- [ ] Layer management for multi-pass plotting.
- [ ] Export/import SVG.
- [ ] Cloud sync (Firebase).
- [ ] Advanced error handling for FluidNC disconnections.
- [ ] Multi-device support (tablet-optimized interface).
Conclusion
PolyPlot bridges the gap between creative drawing tools and precise machine control. By combining React + Fabric.js for editing and FluidNC for hardware communication, it delivers a full plotting workflow in-browser.
The project grew from a simple proof-of-concept into a modular interface, overcoming Fabric.js quirks, WebSocket stability issues, and G-code inefficiencies.
It is now positioned as a teaching-friendly, extensible plotting tool — and a base for further exploration in digital fabrication.