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 toDocuments\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.
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()viaevaluateJavaScript
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.
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
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 runor 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.
To build a .exe installer package (requires Inno Setup 6 from
https://jrsoftware.org/isdl.php):
./tools/build-installer.ps1This 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.
- ✅ Template Designer shell (WPF + WebView2), template CRUD, no printing.
- ✅ Rendering pipeline — see Phase 2 section below.
- ✅ Custom Designer — own window/app, CSV/Excel binding, one-off and bound printing. See Phase 5 section below.
- ✅ 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. - ✅ Engine —
VectorLabel.Engine, a real system-tray app: absorbsVectorLabel.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. - Vectorworks plug-in — already pure Python, should need little to no change.
New project. Ties together everything so far into one testable path:
VectorLabel.Rendering's LabelRenderer → MonoRaster.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/MonoRasterare 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:ControlTransferneededref UsbSetupPacket+IntPtr.Zero(not abyte[]) to match LibUsbDotNet 2.2.29's actual signature.- On Windows, a WinUSB-bound interface opens as
LibUsbDotNet.WinUsb. WinUsbDevice, which does not implementIUsbDevice— that interface (ClaimInterface/SetConfiguration/ReleaseInterface) only exists on the cross-platform libusb-1.0 backends (LibUsbDevice/MonoUsbDevice).Open/ReadTelemetry/Closenow useUsbDeviceas the working type and only call those members when the runtime object actually supportsIUsbDevice, 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'sSKFont.MeasureTextonly takes glyph spans, notstring— switched text-width measurement toSKPaint.MeasureText.
What you need before testing:
- 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_driverdoes automatically).- Printer-class interface (0): on a fresh machine this is normally
claimed by Windows' built-in
usbprintdriver. 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'sUsbDevice.AllDevicesscans 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 —AllDevicesstill won't see interface 1 afterward.M611Usb.csnow works around this by opening interface 1 directly viaWinUsbDevice.GetDevicePathList/Openagainst its known WCID GUID (a hardware-firmware fact, not a per-install value — see the constant's doc comment) rather than relying onAllDevicesfor it. If this GUID ever needs re-deriving on a new unit, read it fromHKLM\SYSTEM\CurrentControlSet\Enum\USB\VID_0E2E&PID_0013&MI_01\...\ Device Parameters\DeviceInterfaceGuid.
- Printer-class interface (0): on a fresh machine this is normally
claimed by Windows' built-in
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 (
BuildMultiPageJobexists and is ported, just not wired intoM611Printeryet). - 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
BradyCatalogyet, soM611Printer.PrintLabeltakes 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.
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 printCustom → PrintSubmitter → the file queue →
VectorLabel.QueueConsumer → M611Printer.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.
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.
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)
- 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 viaVectorLabel.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. areaRotationis hardcoded to 270 inJobProcessor.cs(the same default already hardware-verified in Phase 4'sPrintTest), 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.feedToClearis read but not implemented.JobProcessorlogs a notice and continues rather than silently dropping it or throwing.- Cancel and DetectCassette are real protocol, fake behavior.
M611Printer.PrintRenderedLabelis one blockingM611Usb.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-efforttry/catchspecifically 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
DesignerWindowControlleris 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
ReprintInfosnapshot (including the whole.vlcusJSON), so no schema migration will be needed once that UI exists.
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:
- Startup ordering.
CustomPrintBackend.Start()was called beforeCustomDesignerBridgesubscribed to itsStatusChangedevent, 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 thoughQueueConsumerwas publishing correctly. Fixed by re-pushing the already-knownStatusimmediately after the bridge subscribes. - Wrong thread.
FolderWatcher's debounce timer (aSystem.Threading.Timer) fires on a background thread, butCoreWebView2.ExecuteScriptAsyncrequires the UI thread — every status push after the first was silently swallowed. Fixed with aDispatcher.CheckAccess()/Dispatcher.Invokemarshal at the top of the push method. - 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 thanCustomPrintBackend's 200ms status-file debounce can distinguish from "no change." The JS side's job-status tracking (updateDesignerJobStatusin 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 inJobProcessor.csby pacing status updates usingPrintJobFile. EstLabelMs(the same per-label duration estimatePrintSubmitteralready 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.
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.
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.
- Tray icon + popup (
TrayIcon.cs/PopupWindow.xaml) —NotifyIconhosted from a WPF app (UseWindowsFormsalongsideUseWPFin 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.icoasset anywhere in this solution for a placeholder glyph. - Live status push, not file-polling. Custom Designer has to watch
status/printers.jsonfor changes because it's a separate process from whatever's draining the queue. Engine's popup lives in the same process asStatusPublisher, so it just subscribes to a newStatusPublisher.StatusChangedevent 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, aDispatcher.CheckAccess()/Dispatcher.Invokemarshal at the top of the handler. - Recent Prints — no new persistent store.
PrintQueue.DoneDir/FailedDiralready are a durable, timestamped job history (every file already carries fullRenderedLabels), soRecentPrintsStore.csjust lists the newest N files across both folders (cheapFile. GetLastWriteTimeUtcpass 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.csjust builds a new job with a fresh id/timestamp and the sameRenderedLabels/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 inqueue/(notfailed/) waiting for a printer that happened to be disconnected at the time — matchingJobProcessor's already-proven "no device yet, requeue" behavior rather than erroring. - Actions row —
DesignerAppLauncher.cslaunches the two designer apps by sibling-folder path convention, not a bundle-id or registered install path: when running from source (bin/Debugorbin/Release), it substitutes Engine's own project-folder name in its ownAppContext.BaseDirectoryfor the sibling project's name.Startup Registration.cstoggles launch-at-startup via the standard per-userHKCU\...\Runregistry 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.
- 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.PrintRenderedLabelis still one blockingM611Usb.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-efforttry/catchso 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
MessageBoxon a failed action is enough for v1).
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.
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 toFormulaEngine.swift(and to the JS twin in the HTML).BarcodeRenderer.cs— genuinely exciting one:BarcodeRenderer.swiftruns the same vendoredbwip-js.jsinside JavaScriptCore rather than reimplementing barcode encoding in Swift. So this port just runs that samebwip-js.jsinside Jint (a pure-.NET JS interpreter — no native V8 binary to ship or code-sign) and reproduces the identical__vlBarcodewrapper 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/drawBarcodeto compensate forCGImage'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/csspan) — 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.jsondata (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
justifyalignment 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.
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.