Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VectorLabel for Windows

A native Windows host (WPF + WebView2) for VectorLabel, built by reusing the exact same HTML/JS canvas engine the Mac app uses — copied verbatim, byte-for-byte, from MacApp/Sources/Core/VectorLabelDesigner.html and bwip-js.js. Nothing in those two files has been edited by this port. Only the native shells around them (and the printer driver underneath) are new.

What works right now:

  • Template Designer (VectorLabel.Designer) — draw a reusable, data- bindable label, save it to Documents\VectorLabel\Templates\*.vltmp — the identical folder name, filename rules, and JSON format the Mac app uses, so files are interchangeable between the two platforms as-is.
  • Custom Designer (VectorLabel.CustomDesigner) — one-off or CSV/Excel- bound labels, saved as .vlcus. See Phase 5 below.
  • Engine (VectorLabel.Engine) — the tray-icon app that owns the M611 connection and print queue, with a Recent Prints list and working Reprint. This is what you actually run to enable printing now — see Phase 6 below.
  • Real printing to the M611 over USB, from either designer app, verified on real hardware — see Phase 4, Phase 5, and Phase 6 below.

What's deliberately NOT here yet: Auto Print, and Engine's own Preferences/printer-model-editor/supply-catalog-editor UI (the catalog is already ported and works, just read-only — see Phase 6). See Roadmap below.

Why this was safe to build without touching the HTML

The Mac app's DesignerWindowController.swift and the HTML talk to each other through a narrow, well-defined contract:

  • JS → native: window.webkit.messageHandlers.vectorlabel.postMessage({action, payload})
  • native → JS: calling named globals like initDesignerTemplates(json), initOpenTemplate(json), markClean() via evaluateJavaScript

WebView2's equivalents are window.chrome.webview.postMessage(obj) (JS → host) and ExecuteScriptAsync(...) (host → JS). Rather than edit the HTML, Assets/webkit-shim.js is injected before any page script runs and simply defines window.webkit.messageHandlers.vectorlabel on top of the WebView2 channel — the HTML never knows the difference. This means future updates to VectorLabelDesigner.html on the Mac side can be dropped straight into Assets/ here with no merge work, as long as the message contract doesn't change.

Project layout

VectorLabel.Designer/
  Assets/
    VectorLabelDesigner.html   ← verbatim copy from the Mac repo
    bwip-js.js                 ← verbatim copy from the Mac repo
    webkit-shim.js             ← NEW: WKWebView-bridge shim (see above)
  App.xaml / App.xaml.cs       ← WPF entry point
  MainWindow.xaml / .cs        ← hosts WebView2, bootstraps the bridge
  DesignerBridge.cs            ← C# port of DesignerWindowController's
                                  WKScriptMessageHandler switch
  TemplateStore.cs             ← C# port of TemplateStore.swift (schema-
                                  agnostic: treats templates as opaque JSON
                                  rather than re-modeling every field)
  PrefsStore.cs                ← tiny key-value store for designer prefs

Building & running

Requires Visual Studio 2022 (or dotnet CLI) with the .NET 8 SDK and the WebView2 Runtime (pre-installed on Windows 11 and most Windows 10 machines; if missing, grab the Evergreen Bootstrapper from Microsoft).

cd VectorLabel.Designer
dotnet restore
dotnet run

or just open VectorLabel.Designer.sln in Visual Studio and hit F5 — this now opens the whole solution (Template Designer, Custom Designer, Engine, QueueConsumer, the shared libraries, and PrintTest), not just the one project. Everything in it has since been built and run for real, including end-to-end prints to physical M611 hardware — see the Phase 4, Phase 5, and Phase 6 sections below for what's been verified and what's still a known gap.

To actually print, run VectorLabel.Engine first (it's the tray app — launch it once, it keeps running and owns the M611 connection), then use either designer app. VectorLabel.QueueConsumer still exists and still works standalone (handy for headless/scripted testing without a tray icon), but the two can't run at the same time — see Phase 6.

Building a distributable installer

To build a .exe installer package (requires Inno Setup 6 from https://jrsoftware.org/isdl.php):

./tools/build-installer.ps1

This publishes all three apps and compiles them into dist\VectorLabelSetup.exe. The version stamped into the installer comes from Directory.Build.props's <Version> element — bump that one line to cut a new release.

Roadmap (matches the four-app / Engine architecture)

  1. Template Designer shell (WPF + WebView2), template CRUD, no printing.
  2. Rendering pipeline — see Phase 2 section below.
  3. Custom Designer — own window/app, CSV/Excel binding, one-off and bound printing. See Phase 5 section below.
  4. M611 printer driver (USB) — see Phase 4 section below. Brady i3300 and Brother PT-E560BT drivers have since been added too (both via their installed Windows print driver rather than a raw protocol — see PrinterDriverRegistry.cs). M610 is still not started; the Mac app's own docs mark it as hardware-unconfirmed too.
  5. EngineVectorLabel.Engine, a real system-tray app: absorbs VectorLabel.QueueConsumer's queue-draining role entirely, adds a live status popup, Recent Prints, and a working Reprint. See Phase 6 below for exactly what's in v1 vs. still Mac-only (Preferences window, printer model editor, supply-catalog editor, multi-printer/network-printer support, auto-update). Inter-app IPC (currently macOS-specific) is the same file-based queue described in Phase 5 rather than named pipes.
  6. Vectorworks plug-in — already pure Python, should need little to no change.

Phase 4: the M611 USB driver (VectorLabel.Printing)

New project. Ties together everything so far into one testable path: VectorLabel.Rendering's LabelRendererMonoRaster.Downscale (900dpi master → the M611's native 300dpi) → M611Bitmap (1-bpp BMP + LZ4 + JSON segment framing) → M611Usb (USB bulk transfer) → the printer.

Status: printing is verified working end-to-end on real M611 hardware (dotnet run --project VectorLabel.PrintTest -- --print produced a clean physical label). Getting there required a few fixes beyond the original port — noted below so the history isn't lost.

Confidence levels, file by file (updated after the real build/hardware pass — see git history for the original pre-build estimates):

  • M611Picl.cs, M611Bitmap.cs, MonoRaster.cs — high confidence, confirmed. M611Bitmap/MonoRaster are exercised by the working print path. M611Picl's framing (magic/length header, JSON envelope) is also confirmed correct — but see the telemetry note below on its request contents.
  • LZ4 encoding — confirmed interoperable. The real print came out correctly rendered, so K4os's raw-block output is being decoded fine by the M611's firmware.
  • M611Usb.cs — print path (Open/Send, interface 0) confirmed working. Two real bugs were fixed to get there, both specific to Windows/WinUSB rather than typos:
    • ControlTransfer needed ref UsbSetupPacket + IntPtr.Zero (not a byte[]) to match LibUsbDotNet 2.2.29's actual signature.
    • On Windows, a WinUSB-bound interface opens as LibUsbDotNet.WinUsb. WinUsbDevice, which does not implement IUsbDevice — that interface (ClaimInterface/SetConfiguration/ReleaseInterface) only exists on the cross-platform libusb-1.0 backends (LibUsbDevice/ MonoUsbDevice). Open/ReadTelemetry/Close now use UsbDevice as the working type and only call those members when the runtime object actually supports IUsbDevice, since WinUSB already binds one device node per interface with nothing left to "claim."
    • Also needed a fix in LabelRenderer.cs (Rendering project, not Printing): SkiaSharp 2.88.8's SKFont.MeasureText only takes glyph spans, not string — switched text-width measurement to SKPaint.MeasureText.

What you need before testing:

  1. The M611's USB interfaces need a WinUSB or libusbK driver bound via Zadig — Windows won't expose them to LibUsbDotNet otherwise (this replaces what macOS's libusb_set_auto_detach_kernel_driver does automatically).
    • Printer-class interface (0): on a fresh machine this is normally claimed by Windows' built-in usbprint driver. In Zadig, Options → List All Devices, select the "(Interface 0)" entry, target driver WinUSB, Replace Driver.
    • Vendor/telemetry interface (1): on the one unit this was tested against, Windows had already auto-bound WinUSB to this interface via a Microsoft OS (WCID) descriptor the M611's firmware advertises — but under a different registry mechanism (DeviceInterfaceGuid, singular) than the one LibUsbDotNet's UsbDevice.AllDevices scans for (DeviceInterfaceGUIDs, plural, multi-string). Zadig's own "Upgrade" option (shown instead of "Replace" when it detects an existing WCID-bound driver) does not rewrite this — AllDevices still won't see interface 1 afterward. M611Usb.cs now works around this by opening interface 1 directly via WinUsbDevice.GetDevicePathList/ Open against its known WCID GUID (a hardware-firmware fact, not a per-install value — see the constant's doc comment) rather than relying on AllDevices for it. If this GUID ever needs re-deriving on a new unit, read it from HKLM\SYSTEM\CurrentControlSet\Enum\USB\VID_0E2E&PID_0013&MI_01\...\ Device Parameters\DeviceInterfaceGuid.
  2. dotnet run --project VectorLabel.PrintTest -- --print — enumerates the M611, builds a two-line synthetic test template, sends it, and polls for completion. This is the actual end-to-end test; everything above exists to make this one command work. Printing itself is confirmed working.

Known gap: telemetry/job-status polling doesn't get a response (yet). With the USB-layer fixes above, ReadTelemetry's write to interface 1 succeeds cleanly (ClearHalt + a full 62-byte write, confirmed at the LibUsbDotNet level) — but the M611 sends back nothing on the IN endpoint, even waiting 15s. So this isn't a driver/discovery bug; it's that the SubscribeAllCurrentAndNewProperties request in M611Picl.JobStatusRequest either isn't the byte-for-byte format the firmware expects, or USB isn't a valid transport for this particular PICL operation. This matches the existing caveat in M611Picl.cs's doc comment: the property GUIDs and request shapes were recovered from Brady's Web SDK, and "TCP/USB transport is a validated hypothesis, not from the SDK itself." Resolving it for real would need either Brady's protocol docs or a USB packet capture of the actual Mac/Brady app performing this exchange, to diff against the exact bytes on the wire. Not a blocker in practice — WaitForCompletion already treats a timeout as "status unknown," not "job failed," and the job prints regardless.

Known gap: the M611 occasionally wedges after a handful of prints and goes physically silent — no feed, no motor, no error light — while the software side reports complete success. Every layer of this port thinks the job worked: M611Usb.Send() returns normally, the job file lands in done/ (not failed/), printers.json keeps reporting "status":"ready". There's no exception or error path to catch, because nothing actually failed from Windows' point of view — the USB bulk transfer completed. This points at the printer's own USB/firmware state getting wedged rather than anything in this port's driver code (seen a couple of times in one session, both times while VectorLabel.QueueConsumer had been running for a while and had already sent several jobs successfully). The fix, every time so far: physically unplug the M611's USB cable, wait a couple of seconds, plug it back in, and print again — no app restart needed. Root-causing this for real would need a USB packet capture across a wedge event to see what state the firmware actually lands in; for now, treat "job completed in software but nothing came out" as this issue first, before assuming a code regression.

What's simplified vs. the Mac app (on purpose, to keep this phase scoped):

  • Single label per job, not the multi-page batch path (BuildMultiPageJob exists and is ported, just not wired into M611Printer yet).
  • Job-status is a simple re-poll loop (WaitForCompletion), not the Mac's persistent push-subscription (openSubscription/readSubFrame) — less efficient, much less code, same information.
  • No BradyCatalog yet, so M611Printer.PrintLabel takes explicit printable width/height/DPI rather than a supply part number. The test harness hardcodes BM-32-427's 1"×1" printable area as an example — swap in your actual loaded supply's dimensions.
  • Network transport (TCP:9100/9102) isn't ported — USB only for now, since that's what's in front of you to test against.

Phase 5: Custom Designer + minimal Queue Consumer

Two new apps plus one new shared library, built on top of everything above: VectorLabel.CustomDesigner (the one-off/bound-print app), VectorLabel. QueueConsumer (the minimal print-driving background process), and VectorLabel.Ipc (the file-queue protocol both talk over). A third small project, VectorLabel.Designer.Shared, was split out of VectorLabel. Designer so PrefsStore and a handful of identical bridge action handlers aren't duplicated between Template Designer and Custom Designer.

Status: the full loop is verified working end-to-end on real M611 hardware — open Custom Designer, add a text object, hit Print, and the job flows through printCustomPrintSubmitter → the file queue → VectorLabel.QueueConsumerM611Printer.PrintRenderedLabel and the printer feeds/prints. Verified with VectorLabel.QueueConsumer running as a separate process, exactly as it would in real use.

Same reuse story as Phase 1: Custom Designer hosts the exact same VectorLabelDesigner.html/bwip-js.js as Template Designer (linked, not copied, so there's one file on disk) — window._designerMode='custom' is injected before any page script runs, and the HTML's own already-built-in custom-mode UI (canvas, data-binding panel, print header) takes it from there. Neither HTML file was touched for this phase either.

Why a file queue

Custom Designer never talks to USB or VectorLabel.Printing directly — it writes a PrintJobFile into %LocalAppData%\VectorLabel\ipc\queue\ and a separate process (VectorLabel.QueueConsumer) claims it, encodes it, and sends it to the M611. This mirrors the Mac's Engine split (front-end renders, Engine owns the printer) even though the real Windows Engine doesn't exist yet — building the IPC contract now, against a minimal consumer, means Custom Designer won't need to change when the real Engine replaces QueueConsumer later.

The queue itself (VectorLabel.Ipc/PrintQueue.cs) is a direct structural port of Core/IPC/PrintQueue.swift: atomic queue/processing/done//failed/ folders, atomic writes (temp file + File.Move (overwrite:true)), atomic claims (File.Move(overwrite:false) — the move itself is the lock). RecoverProcessingJobs() sweeps orphaned processing/ entries back to queue/ on startup, so a crash mid-job doesn't strand it. This held up under a throwaway 33-check harness (crash recovery, race protection, lossy per-element JSON decode) before any UI was built on top of it.

Project layout

VectorLabel.Ipc/                 Shared queue protocol + PrintQueue/FolderWatcher — no UI/USB deps
VectorLabel.CustomDesigner/      WPF app: the one-off/bound-print app
  CustomDesignerBridge.cs          JS<->native message handler (custom-mode actions)
  CustomPrintBackend.cs            Watches status/printers.json, writes jobs + control requests
  PrintSubmitter.cs                printCustom payload -> rendered PrintJobFile -> the queue
  CustomDocumentStore.cs           .vlcus save/open (Documents\VectorLabel\Custom\)
  DataSource/                      CsvGridReader, XlsxRecordReader (ClosedXML), RecordGridBuilder
VectorLabel.QueueConsumer/       Console app: the minimal print-queue drain process
  JobProcessor.cs                   Claims a job, calls M611Printer.PrintRenderedLabel per label
  StatusPublisher.cs                Polls M611Usb.Enumerate() every ~3s, publishes printers.json
  ControlHandler.cs                 Drains control/refresh requests (Cancel, DetectCassette)
  SingleInstance.cs                 Named Mutex so two consumers can't fight over one M611
VectorLabel.Designer.Shared/     PrefsStore + shared passthrough bridge actions (both WPF apps)

What's simplified vs. the Mac app (on purpose, to keep this phase scoped)

  • No supply-catalog port. The HTML's own built-in fallback catalog (_BL_FALLBACK) covers this — including the exact BM-32-427/M6-32-427 part already hardware-verified via VectorLabel.PrintTest. If you need a supply size that isn't in that fallback list (this session hit exactly that, testing against 0.9"×0.9" stock with no matching entry — a real physical label came out, just at the wrong size, because the closest available catalog entry was selected instead), that's this gap, not a bug in the print pipeline itself.
  • areaRotation is hardcoded to 270 in JobProcessor.cs (the same default already hardware-verified in Phase 4's PrintTest), not resolved per-job from cassette telemetry like the Mac does. Same root cause as Phase 4's telemetry gap above — die-cut supplies needing a different rotation won't render correctly until that's fixed.
  • feedToClear is read but not implemented. JobProcessor logs a notice and continues rather than silently dropping it or throwing.
  • Cancel and DetectCassette are real protocol, fake behavior. M611Printer.PrintRenderedLabel is one blocking M611Usb.Send() call — not interruptible mid-job — so Cancel is acknowledged (status republished, control-request file deleted) but doesn't actually stop anything in flight. DetectCassette is a no-op for the same reason cassette telemetry doesn't work in Phase 4. Both write/read through best-effort try/catch specifically so a transient file-I/O hiccup on this no-op path can never crash the app — the plumbing is real even though the hardware capability underneath isn't there yet.
  • Single-window, single-document. The Mac's DesignerWindowController is multi-tab; this is single-window/single-document, matching Template Designer's own existing precedent. The biggest scope cut vs. a literal port.
  • No recents persistence. Cold launch always shows an empty recents list in the start dialog; Open…/browse still work for anything saved earlier.
  • Reprint/reopen UI is out of scope (that's genuinely an Engine feature — the Recent Prints panel and its own queue channel), but every submitted job still carries a full ReprintInfo snapshot (including the whole .vlcus JSON), so no schema migration will be needed once that UI exists.

A real bug found and fixed during hardware testing: status-push races

Two bugs, both timing-related, showed up only once a real QueueConsumer and real hardware were in the loop — worth documenting since neither would show up in code review:

  1. Startup ordering. CustomPrintBackend.Start() was called before CustomDesignerBridge subscribed to its StatusChanged event, so the very first status push (the one that would populate the printer list) fired with nobody listening and was lost — Custom Designer showed "No printers connected" forever, even though QueueConsumer was publishing correctly. Fixed by re-pushing the already-known Status immediately after the bridge subscribes.
  2. Wrong thread. FolderWatcher's debounce timer (a System.Threading.Timer) fires on a background thread, but CoreWebView2.ExecuteScriptAsync requires the UI thread — every status push after the first was silently swallowed. Fixed with a Dispatcher.CheckAccess()/Dispatcher.Invoke marshal at the top of the push method.
  3. Jobs faster than the debounce window. M611Usb.Send() only blocks for the USB buffer transfer, not the physical feed/print/cut, so a small job can go started→finished in a few milliseconds — faster than CustomPrintBackend's 200ms status-file debounce can distinguish from "no change." The JS side's job-status tracking (updateDesignerJobStatus in the HTML, which this port doesn't and can't modify) requires seeing a job as active at least once before it will clear the "Queued" chip on completion — if that window is missed, the chip is stuck forever, which is exactly what happened on the first hardware print test. Fixed in JobProcessor.cs by pacing status updates using PrintJobFile. EstLabelMs (the same per-label duration estimate PrintSubmitter already computes for UI progress), so the "active" status stays visible for roughly as long as the label actually takes to print instead of flashing by unseen.

Phase 6: Engine (VectorLabel.Engine)

A real system-tray app. Research into the Mac's Engine (MacApp/Sources/Engine/VectorLabelEngineApp.swift + MenuBarView.swift, ~1,530 of the ~4,600 total Engine-target lines) showed it's substantially more than a queue drainer — multi-printer-model/network-printer management, real mid-print cancellation, a Preferences UI with a printer-model editor and supply-catalog editor, auto-update, crash reporting, cross-process appearance broadcast, a supply-setup wizard. Given this port only ever drives one printer model (M611, USB-only), v1 scope here is narrower and deliberately so: the tray app that replaces QueueConsumer as the thing you actually run, with a working Recent Prints + Reprint — not a port of Preferences/catalog-editor/auto-update.

Status: builds and runs clean, verified at the OS/registry level and via direct hardware-adjacent testing — see the verification note below on one piece that couldn't be screenshot-verified in this environment.

Engine absorbs QueueConsumer, it doesn't run alongside it

JobProcessor, StatusPublisher, ControlHandler, SingleInstance moved out of the VectorLabel.QueueConsumer console app into a new shared library, VectorLabel.QueueConsumer.Core — referenced by both the (unchanged, still useful for headless/dev use) console app and the new VectorLabel.Engine WPF app. Same pattern as the earlier PrefsStore extraction into VectorLabel.Designer.Shared.

Critically, SingleInstance's mutex name (Global\VectorLabel.QueueConsumer) did not change — CustomPrintBackend.Submit()'s "is a consumer running" check needed zero changes in either designer app. Engine acquiring that exact mutex on startup is enough for both designer apps to treat it as "a consumer is running," with no code on their side even aware Engine exists.

What's new vs. what QueueConsumer already had

  • Tray icon + popup (TrayIcon.cs/PopupWindow.xaml) — NotifyIcon hosted from a WPF app (UseWindowsForms alongside UseWPF in the same project — the standard way to get a tray icon in WPF, since WPF has no native tray control). The icon is drawn at runtime (a blue circle + "V") rather than shipping the first binary .ico asset anywhere in this solution for a placeholder glyph.
  • Live status push, not file-polling. Custom Designer has to watch status/printers.json for changes because it's a separate process from whatever's draining the queue. Engine's popup lives in the same process as StatusPublisher, so it just subscribes to a new StatusPublisher.StatusChanged event instead — simpler and more immediate than file-watching would be. Same threading hazard Custom Designer hit earlier (the event fires from a background poll-timer/worker thread, not the UI thread) — fixed the same way, a Dispatcher.CheckAccess()/Dispatcher.Invoke marshal at the top of the handler.
  • Recent Prints — no new persistent store. PrintQueue.DoneDir/ FailedDir already are a durable, timestamped job history (every file already carries full RenderedLabels), so RecentPrintsStore.cs just lists the newest N files across both folders (cheap File. GetLastWriteTimeUtc pass first, full JSON deserialize only for the files that make the cut). Verified against real job history from this session — correctly loaded and sorted 19 real jobs spanning the whole session, oldest to newest, in one pass.
  • Reprint is real, and simpler here than on the Mac. The Mac's reprint reopens the original front-end to reconstruct print-time state (filter/sort/selection). This port's job files already carry fully rendered pixels — that's literally what got sent to the printer — so ReprintSubmitter.cs just builds a new job with a fresh id/timestamp and the same RenderedLabels/PrinterID/CutMode, no front-end round-trip needed. Verified end-to-end against a real prior job from this session: correctly built a new job with a fresh id and submitted it to the queue, which sat correctly in queue/ (not failed/) waiting for a printer that happened to be disconnected at the time — matching JobProcessor's already-proven "no device yet, requeue" behavior rather than erroring.
  • Actions rowDesignerAppLauncher.cs launches the two designer apps by sibling-folder path convention, not a bundle-id or registered install path: when running from source (bin/Debug or bin/Release), it substitutes Engine's own project-folder name in its own AppContext.BaseDirectory for the sibling project's name. Startup Registration.cs toggles launch-at-startup via the standard per-user HKCU\...\Run registry key (simplest available mechanism — no Task Scheduler, no service). Verified directly: SetEnabled(true)/IsEnabled()/ SetEnabled(false) round-tripped correctly against the real registry, including clean removal. When run from the installer (dist\VectorLabelSetup.exe), all three apps live in the same installed directory, so sibling discovery works identically.

What's simplified vs. the Mac app (on purpose, to keep this phase scoped)

  • No Preferences window, no Printer Model Editor, no Supply Catalog Editor. The catalog (SupplyCatalogDefaults.cs) is already ported and works — it just stays read-only, an explicit non-goal carried over from when it was first built.
  • Multi-printer-model support has since landed (M611, Brady i3300, Brother PT-E560BT, all via PrinterDriverRegistry) — this bullet originally said Engine only ever drove one M611; that's no longer true, see the Roadmap section above.
  • Cancel and DetectCassette are still exactly the no-ops they already were. M611Printer.PrintRenderedLabel is still one blocking M611Usb.Send() call per label with no interruption point, and cassette telemetry still gets no USB response on this hardware — both unrelated, pre-existing gaps this phase doesn't touch. Both write/read through best-effort try/catch so a transient I/O hiccup on either no-op path can never crash the app.
  • No auto-update, no crash reporting, no appearance/dark-mode broadcast, no supply-setup wizard, no toast notifications (a MessageBox on a failed action is enough for v1).

A real environment limitation hit during verification

The tray icon and popup could not be visually confirmed via automated screenshot in this session's remote environment — extensive troubleshooting (a minimal isolated NotifyIcon-only test app, not just this project's own code, behaved identically) traced this to the screenshot tool not capturing Explorer's live notification-area rendering in this particular remote session, not a code defect. Direct proof the icon and registration are correct: HKCU\Control Panel\NotifyIconSettings\<id> gets a real entry the moment Shell_NotifyIcon(NIM_ADD) succeeds, keyed by the exe path, and its IconSnapshot value is a literal PNG of the icon Windows has on file for it — extracting and viewing that PNG showed the exact blue-circle-"V" icon this code draws, pixel for pixel. If you're reading this on a machine with normal desktop access, a quick manual check (launch VectorLabel. Engine.exe, look at the tray, click the icon) is worth doing once — the underlying logic is verified as thoroughly as this environment allowed, but an actual human eyes-on click was never possible from here.

Phase 2: the rendering pipeline (VectorLabel.Rendering)

New project, VectorLabel.Rendering — a class library (not tied to WPF) so Auto Print, Custom Designer, and the Engine can all reference it later, mirroring how the Mac app's Core module is shared across all four apps.

Fully ported (faithful, line-by-line translations of the Swift, not reimplementations from the doc comments):

  • FormulaEngine.cs — the =IF(...)/LEFT/RIGHT/MID/etc. formula language. Pure logic, no platform dependencies, so this is the one file I'm confident is byte-for-byte behaviorally identical to FormulaEngine.swift (and to the JS twin in the HTML).
  • BarcodeRenderer.cs — genuinely exciting one: BarcodeRenderer.swift runs the same vendored bwip-js.js inside JavaScriptCore rather than reimplementing barcode encoding in Swift. So this port just runs that same bwip-js.js inside Jint (a pure-.NET JS interpreter — no native V8 binary to ship or code-sign) and reproduces the identical __vlBarcode wrapper and square/stretch module-rasterization algorithm. Same barcode engine, same output, on both platforms.
  • render() orchestration, coordinate math, calibration offset, landscape rotation, and the simple shape drawers: line, rectangle (+ rounded corners), ellipse, triangle, regular polygon, arrow, and image (data-URL PNG decode). All faithful ports of the Swift geometry.
  • A structural simplification worth knowing about: CoreGraphics defaults to a bottom-left/y-up origin, so the Swift renderer flips its whole context once, then flips again locally inside drawImage/drawBarcode to compensate for CGImage's bottom-up convention. SkiaSharp's canvas is top-left/y-down natively — the frame the Swift code is fighting to reach — so this port just draws directly in that frame throughout. Same visual result, less code.

Not yet ported (stubbed with clear comments in the source, not silently dropped):

  • Table merged cells (rs/cs span) — the grid and unmerged cell text render; a merged region's spanning text does not yet union its cells.
  • Feed rotation and the die-cut→continuous re-map — both need BradyCatalog.json data (Phase 4 territory) this port doesn't have yet.
  • Text layout fidelity — the font-size math (autoscale binary search, designer-px→print-px scaling, valign) is a faithful port, but the actual glyph shaping comes from SkiaSharp's text APIs standing in for CoreText's framesetter. Word-wrap here is simple greedy wrapping; CoreText's is more nuanced. True justify alignment isn't implemented (falls back to left). This is the part most worth a side-by-side visual check against the Mac app — I can't render either version from where I'm working to compare.

As of Phase 5, this rendering code is wired into real printing: PrintSubmitter (Custom Designer) calls LabelRenderer.Render directly for every print job. There still isn't a "print preview" button anywhere in either app's UI — rendering only happens at actual print time.

What to do next

Everything through Phase 6 builds clean and has been exercised on real M611, i3300, and PT-E560BT hardware or verified as thoroughly as this environment allowed — see each phase section above for exactly what's verified vs. a known, documented gap (cassette telemetry, feedToClear, Engine's Preferences/catalog-editor scope). The things most worth doing next:

  • A physical eyes-on check of Engine's tray icon and popup — the one piece Phase 6 couldn't verify by screenshot in this session's remote environment (see that section for the registry-level proof it's actually working). A five-second manual click is all that's missing.
  • A side-by-side visual check of text layout against the Mac app — flagged back in Phase 2 and still the single biggest "might not be pixel-identical" risk, since it hasn't been possible to render both platforms side by side during this port.
  • Engine's Preferences window / printer-model editor / supply-catalog editor, if multi-printer or editable-catalog support ever becomes worth it — the catalog itself is already ported and correct, just read-only.
  • Auto Print — not started at all, the last unaddressed item in the four-app suite.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages