<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ahsan Mehmood — Dev Blog]]></title><description><![CDATA[Building real-world software with .NET, Flutter, AI & more]]></description><link>https://iamahsanmehmood.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 18 Sep 2026 20:15:38 GMT</lastBuildDate><atom:link href="https://iamahsanmehmood.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[We Let AI Build SketchUp Models With Nothing But an API]]></title><description><![CDATA[OpenSKP's writer API has no Chair class. No Table builder, no furniture module, nothing that knows what a chair is. It exposes materials, layers, groups, component definitions, and faces — the same pr]]></description><link>https://iamahsanmehmood.hashnode.dev/we-let-ai-build-sketchup-models-with-nothing-but-an-api</link><guid isPermaLink="true">https://iamahsanmehmood.hashnode.dev/we-let-ai-build-sketchup-models-with-nothing-but-an-api</guid><dc:creator><![CDATA[Ahsan Mehmood]]></dc:creator><pubDate>Thu, 20 Aug 2026 13:45:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69af0985af06a097c3bcd000/ec50a3b8-4cb8-4403-9e79-2086af71e8d3.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[


<p><a href="https://github.com/iamahsanmehmood/openskp">OpenSKP</a>'s writer API has no <code>Chair</code> class. No <code>Table</code> builder, no <code>furniture</code> module, nothing that knows what a chair is. It exposes materials, layers, groups, component definitions, and faces — the same primitives SketchUp itself is built from — and nothing above that. Which raised an obvious question once the writer shipped: could an AI coding agent, given only that low-level API and a plain-English description of an object, produce a real, correctly-structured <code>.skp</code> file?</p>
<p>We ran the experiment with two different AI coding agents, independently, with no shared prompt engineering between the runs. Both were pointed at the same generic API and asked to build furniture. Neither was given examples of chair geometry or table dimensions to crib from.</p>
<h2>Chair, table, and an armchair</h2>
<p><img src="https://openskp.com/assets/ai-modeling/chair-table-armchair.png" alt="AI-generated chair, side table, and armchair rendered in the OpenSKP web viewer" />
<em>Output from an AI agent's own generated Python script, loaded straight into the OpenSKP web viewer with no manual cleanup.</em></p>
<p>The first run produced a scene with a dining chair, a side table, and an armchair — nine component definitions, thirty-eight geometry primitives in total, organized across three layers with four distinct materials. That structure wasn't hand-specified; the agent decided the layer and material breakdown itself, deriving it from how it reasoned about the objects (seat vs. legs vs. backrest as separate faces sharing a wood material, for instance).</p>
<p>Here's a representative excerpt of what the agent actually wrote, generating a chair leg as an extruded rectangular profile:</p>
<pre><code class="language-python">def add_leg(group, x, y, z, width, height, material):
    """Add a single tapered leg as a box primitive."""
    pts = [
        (x, y, z),
        (x + width, y, z),
        (x + width, y + width, z),
        (x, y + width, z),
    ]
    top = [(px, py, z + height) for px, py, _ in pts]
    face_bottom = group.add_face(pts, material=material)
    face_top = group.add_face(top, material=material)
    for i in range(4):
        side = [pts[i], pts[(i + 1) % 4], top[(i + 1) % 4], top[i]]
        group.add_face(side, material=material)
    return face_bottom, face_top
</code></pre>
<p>Nothing in that function is chair-specific — it's a general box-extrusion routine, the kind of primitive-geometry reasoning you'd expect from someone who understands 3D coordinate systems, not someone who was handed a chair template. The agent built its own mental model of "chair" out of boxes and faces, the same way a human modeler would work from scratch in SketchUp's own polygon tools.</p>
<h2>An executive desk</h2>
<p><img src="https://openskp.com/assets/ai-modeling/executive-desk.png" alt="AI-generated executive desk with drawers rendered in the OpenSKP web viewer" />
<em>A more complex single object: a desk with a drawer unit, modeled as nested component groups.</em></p>
<p>The second test pushed further into nested structure: a desk with an attached drawer unit, modeled as a component group nested inside the desk's top-level group — mirroring how a careful human SketchUp modeler would organize the same object, with the drawer as its own reusable component rather than geometry welded directly into the desktop.</p>
<h2>A phone, viewed from both sides</h2>
<p><img src="https://openskp.com/assets/ai-modeling/phone-front-back.png" alt="AI-generated phone model shown from front and back in the OpenSKP web viewer" />
<em>Front and back views of the same AI-generated phone model — face winding and normals came out correct on the first attempt.</em></p>
<p>The third object, from the second independent agent run, was a simplified phone: a thin rounded body, a screen face, and a camera module. What's notable here isn't the object's complexity — it's the simplest of the three — but that face winding order came out correct without being told about it explicitly. Get winding backward and a face renders as invisible or inside-out from the "wrong" side; the model shown above renders correctly from both front and back, which means the agent's geometry reasoning implicitly respected consistent counter-clockwise winding, not just "some points that happen to form a face."</p>
<h2>What this does and doesn't prove</h2>
<p>This isn't a claim that AI agents can replace a SketchUp modeler for genuinely complex or organic geometry — everything shown here is bounded, rectilinear furniture-and-electronics geometry, well within what an LLM can reason about symbolically in coordinates and box-extrusions. What it does show is that OpenSKP's writer API is low-level enough, and clean enough, that an agent with no domain-specific tooling can drive it correctly: valid component hierarchies, sane material and layer organization, and geometry SketchUp itself opens without complaint.</p>
<p>That's the actual target for the API design — not "convenient for humans typing by hand," but "reasonable for a coding agent to drive from a plain description," since increasingly, that's who's calling it.</p>
<hr />
<p><em>Try it yourself: <a href="https://github.com/iamahsanmehmood/openskp">github.com/iamahsanmehmood/openskp</a> — MIT licensed, available for Python, TypeScript, .NET, Dart, and C++.</em></p>
]]></content:encoded></item><item><title><![CDATA[OpenSKP 1.1.0: A Native .skp Writer, Now in All Five Languages]]></title><description><![CDATA[Until 1.1.0, OpenSKP was a read path. You could parse a .skp file — either container format, all five languages — and get materials, layers, geometry, and metadata out of it. What you couldn't do was ]]></description><link>https://iamahsanmehmood.hashnode.dev/openskp-1-1-0-a-native-skp-writer-now-in-all-five-languages</link><guid isPermaLink="true">https://iamahsanmehmood.hashnode.dev/openskp-1-1-0-a-native-skp-writer-now-in-all-five-languages</guid><dc:creator><![CDATA[Ahsan Mehmood]]></dc:creator><pubDate>Thu, 20 Aug 2026 13:37:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69af0985af06a097c3bcd000/78e3486b-c28e-497a-a20f-a647c2c5a900.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Until 1.1.0, <a href="https://github.com/iamahsanmehmood/openskp">OpenSKP</a> was a read path. You could parse a <code>.skp</code> file — either container format, all five languages — and get materials, layers, geometry, and metadata out of it. What you couldn't do was make a new one. 1.1.0 closes that gap: a real writer, producing files SketchUp itself opens without complaint, shipped in Python, TypeScript, .NET, Dart, and C++ at the same time.</p>
<h2>Why write support is a different problem than read support</h2>
<p>Parsing forgives sloppiness in a way writing doesn't. A parser that mishandles an obscure edge case just gets slightly wrong data for that one field — annoying, but survivable, and often invisible unless you're specifically checking. A writer that gets the container format wrong produces a file that SketchUp refuses to open at all, or worse, opens with silently corrupted geometry. There's no partial credit.</p>
<p>That asymmetry shaped how the writer got built. It targets the modern <strong>VFF</strong> container exclusively — the ZIP-based, TLV-tree format used by SketchUp 2021 and later — rather than also targeting the legacy MFC stream format, because VFF is what every current SketchUp install expects by default and where new files should live going forward. Every writer feature was validated the same way the parser's correctness was validated originally: produce a file, open it with the real SketchUp SDK, and confirm the SDK's own reading of geometry, materials, and layer assignments matches what was intended, field by field — not just "the file has a size greater than zero and doesn't crash on open."</p>
<h2>What the writer actually does</h2>
<p>The API surface is intentionally generic rather than shaped around specific object types. There's no <code>Chair</code> class or <code>Table</code> builder baked into the library — you compose scenes out of the same primitives SketchUp itself works with:</p>
<ul>
<li><p><strong>Materials</strong> — solid colors, opacity, and image textures</p>
</li>
<li><p><strong>Layers (tags)</strong> — for organizing geometry the way SketchUp's own Tags panel does</p>
</li>
<li><p><strong>Groups and component definitions</strong> — the same grouping/instancing model SketchUp uses natively, including nested groups</p>
</li>
<li><p><strong>Curves and faces</strong> — arbitrary polygon geometry with material and layer assignment per face</p>
</li>
<li><p><code>create()</code> for a brand-new file, and <code>open_existing()</code> for loading a file, editing it, and writing it back out</p>
</li>
</ul>
<p><code>open_existing()</code> in particular was the harder of the two entry points to get right. It isn't a byte-patcher that finds the one chunk you touched and surgically rewrites it in place — that approach is fragile against any structural change (add a face, and every downstream offset in the container shifts). Instead it does a full parse of the existing file into the same in-memory model the writer already knows how to serialize, applies your edits to that model, and replays the entire thing back out through the same write path a brand-new file goes through. Slower than a patch, but correct by construction: if <code>create()</code> is trustworthy, <code>open_existing()</code> inherits that trust instead of needing its own separate proof.</p>
<h2>Five ports, five different sets of bugs</h2>
<p>The writer shipped in Python first, since Python was already the most mature of the five ports and the fastest place to validate the design against the real SDK. Porting it to the other four languages wasn't a mechanical translation exercise — each port's own CI caught real, language-specific problems that the Python reference implementation simply couldn't have surfaced:</p>
<ul>
<li><p><strong>C++</strong> — a <code>check_writable</code> helper failed to compile under the CI's stricter build flags, a class of error Python's dynamic typing has no equivalent to catching until runtime, if at all. The C++ port also turned up a <code>clang-format</code> issue where the CI's diff report was silently truncating on longer violations, which needed fixing in the tooling itself before it could be trusted to gate anything.</p>
</li>
<li><p><strong>Dart</strong> — trigonometric rounding in transform-matrix math didn't match the other languages' output bit-for-bit, traced to a difference in how Dart's math library rounds versus Python's, and fixed by aligning the rounding step explicitly rather than relying on each language's default behavior.</p>
</li>
<li><p><strong>Two of the ports</strong> hit test-ordering bugs — tests that passed individually but failed when run as part of the full suite, because they shared mutable fixture state that Python's test runner happened to isolate in a way the other runner didn't by default.</p>
</li>
</ul>
<p>None of these were writer-logic bugs in the sense of "the geometry is wrong." They were the ordinary friction of porting real, non-trivial code across five different type systems, build toolchains, and test runners — exactly the kind of thing that's invisible if you only ever ship one language, and exactly why keeping five active ports is more expensive than it sounds, but catches more than a single-language project ever would.</p>
<h2>Shipping five packages at once</h2>
<p>1.1.0 went out to all five registries in the same release cycle — PyPI, npm, NuGet, pub.dev, and a tagged GitHub Release with prebuilt C++ artifacts (C++ has no package registry equivalent, so it ships as a downloadable tarball/zip pair instead). Coordinating that many release pipelines in one pass surfaced two more process-level snags worth naming honestly: a batched multi-language tag push needed the tags separated out per-language rather than pushed as one lump, and pub.dev's own publish flow has a gotcha around tag naming that isn't obvious until it rejects a push.</p>
<p>Neither was a code bug. Both are the kind of thing you only learn by actually running a five-language release end to end, which is exactly what 1.1.0 forced.</p>
<h2>What's next</h2>
<p>Read and write both exist now, but write support is currently VFF-only — no legacy-MFC writing, and no re-encoding a legacy file into the modern container. Conversion the other direction (glTF, IFC, OBJ into <code>.skp</code>) is on the roadmap but not started. If either of those is something you'd use, the <a href="https://github.com/iamahsanmehmood/openskp/issues">issue tracker</a> is open.</p>
<hr />
<p><strong>Install:</strong></p>
<pre><code class="language-bash">pip install openskp          # Python
npm install openskp          # TypeScript / JavaScript
dotnet add package OpenSKP   # .NET
dart pub add openskp         # Dart
</code></pre>
<p><em>OpenSKP is open source under the MIT license:</em> <a href="https://github.com/iamahsanmehmood/openskp"><em>github.com/iamahsanmehmood/openskp</em></a></p>
]]></content:encoded></item><item><title><![CDATA[Why We Built OpenSKP]]></title><description><![CDATA[If you want to read a .dxf file, there's an open specification. If you want to read a .gltf file, there's a Khronos Group standard with a public GitHub repo and a validator you can run against your ou]]></description><link>https://iamahsanmehmood.hashnode.dev/why-we-built-openskp</link><guid isPermaLink="true">https://iamahsanmehmood.hashnode.dev/why-we-built-openskp</guid><dc:creator><![CDATA[Ahsan Mehmood]]></dc:creator><pubDate>Thu, 20 Aug 2026 13:32:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69af0985af06a097c3bcd000/63b5aff6-7a92-4e47-b95c-ab75a39e7ae6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you want to read a <code>.dxf</code> file, there's an open specification. If you want to read a <code>.gltf</code> file, there's a Khronos Group standard with a public GitHub repo and a validator you can run against your output. If you want to read a <code>.skp</code> file — SketchUp's own native format — there is nothing. No spec, no schema, no reference decoder. Just a proprietary SDK, licensed by Trimble, that only runs where Trimble lets it run.</p>
<p>That gap is the entire reason <a href="https://github.com/iamahsanmehmood/openskp">OpenSKP</a> exists.</p>
<h2>The problem with "just use the SDK"</h2>
<p>SketchUp's official SDK is a real, functional way to read and write <code>.skp</code> files — if your project can tolerate everything that comes with it: a native dependency tied to specific platforms, a license that constrains how and where you can ship, and no path at all if you're building for Linux servers, WebAssembly, or anywhere the SDK simply isn't available.</p>
<p>That constraint is exactly what two of the projects OpenSKP now powers ran into. FrameSmart, a 3D collaboration platform, needed to parse SketchUp files as part of a Linux-hosted pipeline. IngeTrazo, a Linux-first 3D modeler for civil engineering, was running the real SketchUp SDK through Wine — a working setup, but a fragile one, dragging a Windows-only dependency into a project that had no other reason to need it.</p>
<p>Neither of those is an unusual situation. Anyone building a pipeline tool, a headless converter, a web-based viewer, or a CI step that touches SketchUp files runs into the same wall: the only "real" way in is a native SDK that assumes you're building a desktop plugin on Windows or macOS.</p>
<h2>Reverse-engineering, not guessing</h2>
<p>Without a spec, the only way to understand the format is to look at real bytes and figure out what they mean — and then <em>prove</em> the theory rather than just believing it looks plausible. That distinction matters more than it sounds. A parser that produces geometry that <em>looks</em> right in a debug print is not the same as a parser that's actually correct; SketchUp files are dense binary structures where a single misread flag byte can silently corrupt geometry without ever throwing an error.</p>
<p>The methodology that held up: build real files with the actual SketchUp application (or, once OpenSKP's own writer existed, the real SDK as a validation oracle), then diff OpenSKP's understanding of those files against what SketchUp itself reports through its own API — material colors, transparency values, transform matrices, vertex positions, all checked field by field rather than assumed. Several real bugs were only caught this way: a legacy-format alpha byte that four of the five language ports were silently discarding, a face's texture-positioning data that was being parsed but never linked back to the face it belonged to, a slot-numbering edge case that corrupted any file crossing a specific size threshold. None of those would have shown up in a "does it produce a mesh" smoke test. All of them showed up the moment real SketchUp was used as the source of truth.</p>
<h2>Two container formats, not one</h2>
<p>Part of what makes this format genuinely hard is that it isn't one format — it's two. SketchUp changed its internal container completely in the 2021 release. Files from SketchUp 2021 onward use <strong>VFF</strong>, a ZIP-based container wrapping a TLV (Tag-Length-Value) binary tree. Files from SketchUp 2013–2020 use something structurally unrelated: a classic MFC <code>CArchive</code> object-graph stream, with its own class-reference and back-reference numbering scheme, no ZIP involved at all.</p>
<p>A tool that only reads one of these covers a shrinking slice of the real files people actually have sitting on disk — architecture firms, civil engineering practices, and product designers routinely have SketchUp libraries stretching back a decade. OpenSKP reads both, transparently, behind the same <code>parse()</code> call in every language, which is a meaningfully larger reverse-engineering effort than picking the newer, better-documented-by-inference format and calling it done.</p>
<h2>Why five languages, and why not bindings</h2>
<p>OpenSKP isn't a core parser in one language with thin wrapper bindings for the rest. Python, TypeScript, .NET, Dart, and C++ are five independent implementations of the same reverse-engineered format, each idiomatic to its own ecosystem — because a Python native extension is a poor fit for a browser-based TypeScript viewer, and a JavaScript parser is a poor fit for a native C++ desktop tool.</p>
<p>The tradeoff is real: five implementations mean five places a bug can hide, and cross-language parity has to be actively maintained rather than assumed. In practice that means every non-trivial fix gets checked against all five ports' actual source before being called complete, and the same real <code>.skp</code> fixtures get run through every language to confirm they produce identical geometry, layers, and materials — not just "each one compiles and returns something."</p>
<h2>Where it stands now</h2>
<p>What started as a read-only reverse-engineering project has grown into a full toolkit: parsing both container formats, converting to seven other formats natively (glTF, OBJ, STL, PLY, DXF, IFC4, JSON), and — as of the 1.1.0 release — writing genuinely new <code>.skp</code> files from scratch, in all five languages, with no SketchUp SDK involved at any point.</p>
<p>None of it required Trimble's permission, a license fee, or a Windows machine. That was always the point.</p>
<hr />
<p><em>OpenSKP is open source under the MIT license:</em> <a href="https://github.com/iamahsanmehmood/openskp"><em>github.com/iamahsanmehmood/openskp</em></a></p>
]]></content:encoded></item><item><title><![CDATA[Building OpenSKP: How We Open-Sourced a Multi-Language 3D SketchUp Parser]]></title><description><![CDATA[Reading or viewing SketchUp (.skp) files in custom apps has traditionally been a closed shop. Developers either had to pay for proprietary cloud APIs or deploy Trimble's official C++ SDK natively on s]]></description><link>https://iamahsanmehmood.hashnode.dev/building-openskp-how-we-open-sourced-a-multi-language-3d-sketchup-parser</link><guid isPermaLink="true">https://iamahsanmehmood.hashnode.dev/building-openskp-how-we-open-sourced-a-multi-language-3d-sketchup-parser</guid><dc:creator><![CDATA[Ahsan Mehmood]]></dc:creator><pubDate>Wed, 24 Jun 2026 08:52:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69af0985af06a097c3bcd000/734afaf8-c9a2-40bc-a7aa-e25985c0a71d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Reading or viewing SketchUp (<code>.skp</code>) files in custom apps has traditionally been a closed shop. Developers either had to pay for proprietary cloud APIs or deploy Trimble's official C++ SDK natively on servers, which is notoriously difficult to scale in serverless environments or run in web browsers.</p>
<p>To solve this, we built <strong>OpenSKP</strong> — a pure, open-source binary parser and WebGL viewer for modern SketchUp (v2021+) files.</p>
<p>In this article, I want to take you behind the scenes of how we reverse-engineered the binary format, resolved 3D planar triangulation, and structured a multi-language library to run natively across <strong>Python, TypeScript, Dart, and C#</strong>.</p>
<hr />
<h3>The VFF Container and Binary TLV Parser</h3>
<p>A modern SketchUp file uses a <strong>Versioned File Format (VFF)</strong>. It is a ZIP container wrapped in a 16-byte custom header.</p>
<p>By finding the byte offset of the ZIP file marker (<code>PK\x03\x04</code>), we extract the contents directly into memory to read <code>model.dat</code> — the core binary database containing geometry and instance metadata.</p>
<p><code>model.dat</code> is structured as a tree of <strong>Tag-Length-Value (TLV)</strong> nodes. To unpack the model, the parser recursively navigates this tree:</p>
<pre><code class="language-mermaid">sequenceDiagram
    participant SKP as .skp File
    participant VFF as VFF Header Reader
    participant ZIP as In-Memory ZIP Extractor
    participant TLV as Recursive TLV Parser
    participant Geom as Geometry Builder (Vertices/Edges/Faces)
    participant Scene as 3D Scene Graph (GLB/glTF)

    SKP-&gt;&gt;VFF: Open Binary Stream
    VFF-&gt;&gt;VFF: Parse Magic (FF FE FF 0E) and Version
    VFF-&gt;&gt;ZIP: Find ZIP Offset (PK\x03\x04)
    ZIP-&gt;&gt;ZIP: Extract model.dat &amp; materials XMLs
    ZIP-&gt;&gt;TLV: Load model.dat payload
    loop Recursive Parse
        TLV-&gt;&gt;TLV: Read Tag (2B) + Size (4B) + Value
    end
    TLV-&gt;&gt;Geom: Map Tags (C409 Verts, B80B Edges, AC0D Faces)
    Geom-&gt;&gt;Scene: Triangulate &amp; Transform meshes (3x4 matrices)
    Scene-&gt;&gt;Scene: Package GLB &amp; Metadata JSON
</code></pre>
<hr />
<h3>Key Technical Challenges</h3>
<h4>1. 3D Planar Triangulation</h4>
<p>SketchUp files store faces as n-sided planar polygons, which can have nested inner holes (like windows inside walls). Since GPUs only render triangles, we had to build a robust triangulation pipeline:</p>
<ol>
<li><p>Extract the face normal vector from the <code>AD0D</code> tag.</p>
</li>
<li><p>Project the 3D vertices onto a local 2D plane perpendicular to the normal.</p>
</li>
<li><p>Apply a 2D triangulation algorithm (using Earcut for JavaScript and Shapely for Python) to resolve boundaries and holes.</p>
</li>
<li><p>Project the index results back into 3D space.</p>
</li>
</ol>
<h4>2. Multi-Language Consistency</h4>
<p>Since developers work in different environments, we wanted OpenSKP to run natively in their preferred language. We structured the monorepo into 4 packages:</p>
<ul>
<li><p><strong>packages/python</strong>: A pure Python library, perfect for Django/Flask/FastAPI backends and serverless conversion tasks.</p>
</li>
<li><p><strong>packages/typescript</strong>: Compiled to ES modules, allowing developers to run the entire parser client-side in the browser.</p>
</li>
<li><p><strong>packages/dart</strong>: Native Dart library enabling mobile and desktop Flutter viewers.</p>
</li>
<li><p><strong>packages/dotnet</strong>: Standard .NET library for enterprise C# engineering pipelines.</p>
</li>
</ul>
<hr />
<h3>How to Use OpenSKP in Your Projects</h3>
<h4>Python 🐍</h4>
<pre><code class="language-bash">pip install openskp
</code></pre>
<pre><code class="language-python">from openskp import SkpFile
from openskp.export import glb

# Parse SKP
skp = SkpFile.open("model.skp")
model = skp.parse()

# Save as GLB
glb.export(skp, "output.glb")
</code></pre>
<h4>TypeScript / JavaScript 🌐</h4>
<pre><code class="language-bash">npm install openskp
</code></pre>
<pre><code class="language-javascript">import { parseSkp } from 'openskp';

// Parse array buffer directly in browser
const model = parseSkp(arrayBuffer);
console.log("Layers:", model.layers);
</code></pre>
<h4>Dart / Flutter 🎯</h4>
<pre><code class="language-yaml">dependencies:
  openskp: ^0.2.0
</code></pre>
<h4>.NET / C# 💻</h4>
<pre><code class="language-bash">dotnet add package OpenSkp
</code></pre>
<hr />
<h3>Battle-Tested in Production</h3>
<p>OpenSKP isn't just a prototype; it is already running in production to parse and visualize framing panels, studs, and trusses in real-time at <a href="https://frame-smart.com/"><strong>Frame-Smart</strong></a>!</p>
<p>Special thanks to <strong>Noor Ali Qureshi</strong> (<a href="https://github.com/nooraliqureshi">@nooraliqureshi</a>) for contributing critical parsing fixes that resolved component instance and material rendering bugs across multiple SketchUp file versions.</p>
<hr />
<h3>Star Us on GitHub! ⭐️</h3>
<p>OpenSKP is released under the MIT License. We welcome contributions from the community to expand the parser's capabilities.</p>
<p>👉 <a href="https://github.com/iamahsanmehmood/openskp"><strong>Join us on GitHub: https://github.com/iamahsanmehmood/openskp</strong></a></p>
]]></content:encoded></item><item><title><![CDATA[I Built the First Deterministic Urdu Compound Word Detector — Here's Why It Took a Full Library to Get There]]></title><description><![CDATA[Urdu is spoken by over 230 million people. It is the national language of Pakistan, one of the 22 scheduled languages of India, and the lingua franca of a diaspora spanning three continents. And yet, ]]></description><link>https://iamahsanmehmood.hashnode.dev/i-built-the-first-deterministic-urdu-compound-word-detector-here-s-why-it-took-a-full-library-to-get-there</link><guid isPermaLink="true">https://iamahsanmehmood.hashnode.dev/i-built-the-first-deterministic-urdu-compound-word-detector-here-s-why-it-took-a-full-library-to-get-there</guid><category><![CDATA[nlp]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[.NET]]></category><dc:creator><![CDATA[Ahsan Mehmood]]></dc:creator><pubDate>Mon, 27 Apr 2026 18:41:23 GMT</pubDate><content:encoded><![CDATA[<p>Urdu is spoken by over 230 million people. It is the national language of Pakistan, one of the 22 scheduled languages of India, and the lingua franca of a diaspora spanning three continents. And yet, if you try to build Urdu software today — real software, not a toy — you will hit the same wall every other developer hit before you: the tools do not exist.</p>
<p>I hit that wall building <a href="https://hamaariurdu.com">HamaariUrdu</a>, an Urdu language learning platform. This post is about what I built to fix it.</p>
<hr />
<h2>The bugs that no library could fix</h2>
<p>I was not looking to build a library. I was looking to ship features. But the bugs kept piling up, and none of the available Urdu NLP libraries (UrduHack, URDUNLP, or anything else) could fix them.</p>
<p><strong>Bug 1: Search returning zero results for words that are obviously in the database.</strong></p>
<p>The database stored <code>ہے</code> using the correct Urdu <code>ہ</code> (U+06C1, Heh Goal). The user's keyboard typed Arabic <code>ه</code> (U+0647, Heh). Both look <strong>completely identical</strong> on screen in Naskh fonts. But <code>U+06C1 !== U+0647</code>. Zero results. No error. No warning. Just silence.</p>
<p><strong>Bug 2: String equality silently failing.</strong></p>
<pre><code class="language-javascript">"قلم" === "قلم"  // false — why?!
</code></pre>
<p>One of those strings was copied from Microsoft Word and contains an invisible ZWNJ (Zero Width Non-Joiner, U+200C) that Word inserts automatically. You cannot see it. Your editor does not show it. But the comparison fails.</p>
<p><strong>Bug 3: TinyMCE destroying Izafat.</strong></p>
<p>In Urdu grammar, Izafat (اضافت) is a grammatical construction that links two words — like the English "of" but expressed as a marker on the first word. The marker is often an apostrophe-like character (U+2019, Right Single Quotation Mark).</p>
<p>TinyMCE — a very popular rich text editor — silently converts U+2019 to <code>&amp;rsquo;</code> before saving. So a word like <code>کتابِ</code> (with Kasra) or a phrase using Izafat apostrophe gets stored as an HTML entity. Every compound word lookup in the database then fails because the stored form doesn't match the queried form.</p>
<p><strong>Bug 4: Numbers overflowing.</strong></p>
<p>Urdu text frequently references South Asian scale: لاکھ (100,000), کروڑ (10,000,000), ارب (1,000,000,000). These are real everyday numbers in Pakistan — newspaper headlines, financial documents, government statistics.</p>
<p><code>Number.MAX_SAFE_INTEGER</code> is 9,007,199,254,740,991. A single کھرب (1 trillion) value loses precision with <code>typeof number</code>. JavaScript silently gives you the wrong answer.</p>
<p><strong>Bug 5: Sorting broken for every Urdu word list.</strong></p>
<p>No database and no JavaScript runtime has native Urdu collation. The Urdu alphabet has 39 letters in a specific order that does not match either Unicode codepoint order or any Latin-derived collation. Every sorted word list was wrong.</p>
<p><strong>Bug 6 — the worst one: Compound words destroying every downstream NLP task.</strong></p>
<p>This one deserves its own section.</p>
<hr />
<h2>The compound word problem</h2>
<p>Urdu مرکب الفاظ (compound words) are multi-word expressions that function as <strong>a single semantic unit</strong> but are written with <strong>spaces between their parts</strong>.</p>
<pre><code>کتاب خانہ  →  library  (کتاب = book, خانہ = place)
بے عزت     →  disrespectful  (بے = without, عزت = honor)
خوش قسمت  →  fortunate  (خوش = well, قسمت = fate)
علم و عمل  →  knowledge and practice  (fixed expression)
محنت مشقت →  hard work  (synonym compound)
</code></pre>
<p>A naive tokenizer sees spaces and splits them. The result:</p>
<pre><code>Input:   "اس نے کتاب خانہ بنایا"
                ↑ ↑
         space between compound components

Wrong:   ['اس', 'نے', 'کتاب', 'خانہ', 'بنایا']
         (5 tokens — "library" is split into "book" + "place")

Right:   ['اس', 'نے', 'کتاب‌خانہ', 'بنایا']
         (4 tokens — "library" is one semantic unit)
</code></pre>
<p>The consequences ripple into every downstream NLP task:</p>
<table>
<thead>
<tr>
<th>Task</th>
<th>What breaks</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Search</strong></td>
<td><code>کتاب خانہ</code> doesn't match <code>کتاب‌خانہ</code> — zero results</td>
</tr>
<tr>
<td><strong>NER</strong></td>
<td><code>امورِ خانہ داری</code> (household affairs) split into 3 unrelated tokens</td>
</tr>
<tr>
<td><strong>Sentiment</strong></td>
<td><code>بے عزت</code> (disrespectful) vs <code>بے</code> + <code>عزت</code> — polarity lost</td>
</tr>
<tr>
<td><strong>Translation</strong></td>
<td><code>رنگ برنگے</code> (colorful) translated as "color" + unknown</td>
</tr>
<tr>
<td><strong>Word count</strong></td>
<td>Every compound inflates the count with phantom tokens</td>
</tr>
</tbody></table>
<h3>Why this is genuinely hard</h3>
<p>Urdu compound words span <strong>four different morphological strategies simultaneously</strong>:</p>
<p><strong>Strategy 1 — Affix-based:</strong> One word contains a known derivational morpheme (prefix or suffix):</p>
<pre><code>کتاب + خانہ   →  library   (خانہ = "place of" suffix)
بے + عزت      →  disrespectful  (بے = "without" prefix)  
خوش + قسمت   →  fortunate  (خوش = "well" prefix)
کتاب + داری   →  librarianship  (داری = "keeping" suffix)
</code></pre>
<p><strong>Strategy 2 — Izafat:</strong> A grammatical linking marker appears in the text, written or implied:</p>
<pre><code>کتابِ حسنہ    (the good book)  — Zer mark (◌ِ) on first word
روحِ رواں     (driving spirit) — Hamza-above (◌ٔ) marker
علم و عمل     (knowledge and practice) — Vav-e-atf (و) connector
</code></pre>
<p><strong>Strategy 3 — Lexical:</strong> Neither word is morphologically special. You simply have to <em>know</em> these pairs:</p>
<pre><code>محنت مشقت     (hard work — synonym compound)
رنگ برنگے     (colorful — echo compound)
صبر شکر       (patient gratitude — near-synonym pair)
انسائیکلوپیڈیا آف اسلام  (3-word fixed title)
</code></pre>
<p><strong>Strategy 4 — Chains:</strong> Three or more words where each link is independently valid:</p>
<pre><code>امورِ خانہ داری  (household affairs — 3 words)
↑       ↑   ↑
izafat  affix  suffix

Decomposition:
امورِ + خانہ  →  izafat compound
خانہ + داری  →  affix compound
Merged:  امورِ خانہ داری  →  one 3-word compound
</code></pre>
<p>No statistical model trained on general text reliably covers all four strategies. They operate at different linguistic levels and require different detection mechanisms.</p>
<hr />
<h2>The approach: three deterministic layers</h2>
<p>Every other Urdu compound detection library (where one even exists) treats this as a <strong>machine learning problem</strong>. They feed training data into statistical models and hope the probabilities align.</p>
<p>That means:</p>
<ul>
<li>Results change unpredictably between corpus versions</li>
<li>You cannot explain <em>why</em> a pair was or wasn't detected</li>
<li>Edge cases (literary izafat, 3-word expressions, echo words) fail silently</li>
<li>No deterministic guarantee across identical inputs</li>
</ul>
<p><strong>urdu-tools takes the opposite approach.</strong> Every detection is grounded in one of three verifiable, explainable rules:</p>
<pre><code>Raw text
   │
   ├─► Layer 1 — Affix (UAWL)
   │       100+ known Urdu prefix/suffix morphemes
   │       خانہ  گاہ  پرست  بے  نا  خوش  شب  غم  …
   │
   ├─► Layer 2 — Izafat
   │       zer mark (◌ِ) · hamza-above (◌ٔ) · vav-e-atf (و)
   │       کتابِ حسنہ · روحِ رواں · علم و عمل
   │
   └─► Layer 3 — Lexicon
           3,262 root entries · N-word tails · greedy longest-match
           محنت مشقت · رنگ برنگے · انسائیکلوپیڈیا آف اسلام
               │
               └─► Span chaining
                       امورِ خانہ  +  خانہ داری  →  امورِ خانہ داری
</code></pre>
<p><strong>The same input always produces the same output, always with a reason.</strong></p>
<p>This is the first open-source implementation of deterministic, multi-layer, N-gram Urdu compound detection in any language.</p>
<hr />
<h2>Introducing urdu-tools</h2>
<p><strong><a href="https://github.com/iamahsanmehmood/urdu-tools">github.com/iamahsanmehmood/urdu-tools</a></strong></p>
<p>A production-quality, zero-dependency Urdu text processing library. Available for TypeScript/JavaScript and C#/.NET, with identical APIs in both.</p>
<pre><code class="language-bash">npm install @iamahsanmehmood/urdu-tools
</code></pre>
<pre><code class="language-bash">dotnet add package UrduTools.Core
</code></pre>
<p>392 tests passing. 85 C# tests. 90%+ coverage enforced in CI.</p>
<hr />
<h2>The compound detection API</h2>
<pre><code class="language-typescript">import {
  detectCompounds,
  joinCompounds,
  splitCompounds,
  isCompound
} from '@iamahsanmehmood/urdu-tools/compound'
</code></pre>
<h3>Detecting compounds</h3>
<pre><code class="language-typescript">// Layer 1: Affix — خانہ is a known place-suffix
detectCompounds('کتاب خانہ بہت اچھا ہے')
// → [{
//     text: 'کتاب خانہ',
//     type: 'affix',
//     components: ['کتاب', 'خانہ'],
//     start: 0,
//     end: 1
//   }]

// Layer 1: Affix — بے is a known privative prefix
detectCompounds('بے عزت آدمی نہیں چاہیے')
// → [{ text: 'بے عزت', type: 'affix', components: ['بے', 'عزت'], ... }]

// Layer 2: Izafat — standalone و (vav-e-atf) between content words
detectCompounds('علم و عمل ضروری ہے')
// → [{ text: 'علم و عمل', type: 'izafat', components: ['علم', 'و', 'عمل'], ... }]

// Layer 3: Lexicon — echo compound, neither word is an affix
detectCompounds('رنگ برنگے پھول کھلے ہیں')
// → [{ text: 'رنگ برنگے', type: 'lexicon', components: ['رنگ', 'برنگے'], ... }]

// Lexicon: synonym compound
detectCompounds('محنت مشقت کے بغیر کامیابی نہیں')
// → [{ text: 'محنت مشقت', type: 'lexicon', ... }]

// 3-word chain: izafat (zer on امورِ) + affix (داری suffix on خانہ)
detectCompounds('امورِ خانہ داری چلانا مشکل ہے')
// → [{ text: 'امورِ خانہ داری', type: 'affix', components: ['امورِ', 'خانہ', 'داری'], ... }]

// 3-word lexicon entry: greedy longest-match wins over any 2-word overlap
detectCompounds('انسائیکلوپیڈیا آف اسلام کا حوالہ')
// → [{ text: 'انسائیکلوپیڈیا آف اسلام', type: 'lexicon', ... }]
</code></pre>
<h3>The pipeline: join before tokenize</h3>
<p>The critical downstream use case — bind compounds <em>before</em> tokenizing:</p>
<pre><code class="language-typescript">import { joinCompounds } from '@iamahsanmehmood/urdu-tools/compound'
import { tokenize } from '@iamahsanmehmood/urdu-tools'

const text = 'کتاب خانہ میں علم و عمل کی کتابیں ہیں'

// Without compound joining — naive tokenizer splits everything
tokenize(text)
// → ['کتاب', 'خانہ', 'میں', 'علم', 'و', 'عمل', 'کی', 'کتابیں', 'ہیں']
//    ↑ split!                 ↑ split!

// With compound joining — semantic integrity preserved
const joined = joinCompounds(text)
// → 'کتاب‌خانہ میں علم‌و‌عمل کی کتابیں ہیں'
//          ↑ ZWNJ (invisible, prevents tokenizer split)

tokenize(joined)
// → ['کتاب‌خانہ', 'میں', 'علم‌و‌عمل', 'کی', 'کتابیں', 'ہیں']
//    ↑ one token            ↑ one token  ✓
</code></pre>
<p>The ZWNJ (Zero Width Non-Joiner, U+200C) is invisible but meaningful — the tokenizer sees it and keeps the word intact.</p>
<h3>Pair-level check</h3>
<pre><code class="language-typescript">isCompound('کتاب', 'خانہ')    // → { matched: true,  type: 'affix'   }
isCompound('محنت', 'مشقت')    // → { matched: true,  type: 'lexicon' }
isCompound('اخلاقِ', 'حسنہ')  // → { matched: true,  type: 'izafat' }
isCompound('اچھا', 'آدمی')    // → { matched: false, type: null      }
</code></pre>
<h3>Fine-grained control</h3>
<pre><code class="language-typescript">// Use only specific layers
detectCompounds(text, { affix: true, izafat: false, lexicon: false })
detectCompounds(text, { affix: false, izafat: true, lexicon: true })

// Choose the binder character for joinCompounds
joinCompounds(text)                      // ZWNJ U+200C (default, invisible)
joinCompounds(text, { binder: 'nbsp' })  // Non-breaking space (visible)
joinCompounds(text, { binder: 'wj' })   // Word Joiner U+2060 (never line-breaks)

// Inverse — split back to spaces
splitCompounds('کتاب‌خانہ')  // → 'کتاب خانہ'
</code></pre>
<hr />
<h2>The normalization pipeline</h2>
<p>A 12-layer normalization pipeline — the foundation that every other module builds on.</p>
<pre><code class="language-typescript">import { normalize, fingerprint } from '@iamahsanmehmood/urdu-tools'
</code></pre>
<table>
<thead>
<tr>
<th>Layer</th>
<th>What it does</th>
<th>Default</th>
</tr>
</thead>
<tbody><tr>
<td>1 — NFC</td>
<td>Unicode canonical form</td>
<td>✅</td>
</tr>
<tr>
<td>2 — NBSP</td>
<td>Non-breaking space → regular space</td>
<td>✅</td>
</tr>
<tr>
<td>3 — Alif Madda</td>
<td><code>آ</code> → <code>آ</code> (precomposed)</td>
<td>✅</td>
</tr>
<tr>
<td>4 — Numerals</td>
<td><code>٠–٩</code> and <code>۰–۹</code> → ASCII <code>0–9</code></td>
<td>✅</td>
</tr>
<tr>
<td>5 — Zero-width</td>
<td>Strip ZWNJ, ZWJ, soft hyphen</td>
<td>✅</td>
</tr>
<tr>
<td>6 — Diacritics</td>
<td>Strip zabar, zer, pesh, shadda, sukun, tanwin</td>
<td>✅</td>
</tr>
<tr>
<td>7 — Honorifics</td>
<td>Strip Islamic honorific signs (ؐ ؑ ؒ ؓ ؔ)</td>
<td>✅</td>
</tr>
<tr>
<td>8 — Hamza</td>
<td><code>أ</code> → <code>ا</code>, <code>ؤ</code> → <code>و</code></td>
<td>✅</td>
</tr>
<tr>
<td>9 — Kashida</td>
<td>Strip tatweel U+0640</td>
<td>❌</td>
</tr>
<tr>
<td>10 — Presentation forms</td>
<td>Map U+FB50–FEFF to base chars</td>
<td>❌</td>
</tr>
<tr>
<td>11 — Punctuation trim</td>
<td>Strip leading/trailing non-letter chars</td>
<td>❌</td>
</tr>
<tr>
<td>12 — Char normalize</td>
<td>Arabic look-alikes → correct Urdu codepoints</td>
<td>❌</td>
</tr>
</tbody></table>
<pre><code class="language-typescript">normalize('عِلمٌ')                    // 'علم'  (layers 1–6: diacritics stripped)
normalize('آ')             // 'آ'    (layer 3: Alif + Madda → precomposed)
normalize('علم‌ہے')             // 'علمہے' (layer 5: ZWNJ stripped)
normalize('نبیؐ')                    // 'نبی'  (layer 7: honorific stripped)

// Full normalization for search indexing
normalize(userInput, {
  kashida: true,
  presentationForms: true,
  punctuationTrim: true,
  normalizeCharacters: true,   // ي → ی, ك → ک, ه → ہ
})
</code></pre>
<h3>The fingerprint function</h3>
<p>For client-side word comparison without database round-trips:</p>
<pre><code class="language-typescript">fingerprint('عِلمٌ') === fingerprint('عَلم')   // true (both normalize to 'علم')
fingerprint('نبیؐ') === fingerprint('نبی')     // true (honorific stripped)
fingerprint('علم‌') === fingerprint('علم') // true (ZWNJ stripped)
</code></pre>
<p>We use this in HamaariUrdu to compare user input against stored words in a 110,000+ word dictionary without needing a round-trip to the database for every keystroke.</p>
<hr />
<h2>The Arabic–Urdu confusion problem</h2>
<p>This is the <strong>single most common source of silent failures</strong> in Urdu software, and no existing library addressed it.</p>
<p>Three character pairs are <strong>visually identical</strong> in Naskh fonts but are different Unicode code points:</p>
<table>
<thead>
<tr>
<th>Visual</th>
<th>Arabic codepoint</th>
<th>Urdu codepoint</th>
<th>Common source</th>
</tr>
</thead>
<tbody><tr>
<td>ی</td>
<td>ي U+064A</td>
<td>ی U+06CC</td>
<td>Arabic-layout keyboards, Arabic websites</td>
</tr>
<tr>
<td>ک</td>
<td>ك U+0643</td>
<td>ک U+06A9</td>
<td>Arabic-layout keyboards</td>
</tr>
<tr>
<td>ہ</td>
<td>ه U+0647</td>
<td>ہ U+06C1</td>
<td>Arabic text pasted into Urdu context</td>
</tr>
</tbody></table>
<p>A user searching for <code>ہے</code> typed with Arabic <code>ه</code> finds <strong>zero results</strong> in a database that stored it with Urdu <code>ہ</code>. Both look identical on screen. No error. No warning. Zero results.</p>
<pre><code class="language-typescript">import { normalizeCharacters } from '@iamahsanmehmood/urdu-tools'

normalizeCharacters('ي')  // → 'ی'  (U+064A → U+06CC)
normalizeCharacters('ك')  // → 'ک'  (U+0643 → U+06A9)
normalizeCharacters('ه')  // → 'ہ'  (U+0647 → U+06C1)

// Apply before storage or search indexing:
normalize(userInput, { normalizeCharacters: true })
</code></pre>
<hr />
<h2>Progressive search matching</h2>
<p>The search module tries 9 progressively aggressive normalization layers until it finds a match — or returns false with full diagnostic info.</p>
<pre><code class="language-typescript">import { match, fuzzyMatch, getAllNormalizations } from '@iamahsanmehmood/urdu-tools'

match('عِلمٌ', 'علم')
// → { matched: true, layer: 'strip-diacritics', normalizedQuery: 'علم', normalizedTarget: 'علم' }

match('نبیؐ', 'نبی')
// → { matched: true, layer: 'strip-honorifics', ... }

match('أحمد', 'احمد')
// → { matched: true, layer: 'normalize-hamza', ... }

match('کتاب', 'علم')
// → { matched: false, layer: null, ... }
</code></pre>
<p>For database lookups, <code>getAllNormalizations()</code> returns every normalized form to try:</p>
<pre><code class="language-typescript">const forms = getAllNormalizations('عِلمٌ')
// → ['عِلمٌ', 'عِلم', 'علم', ...]  (from most specific to most aggressive)

for (const form of forms) {
  const result = await db.get(form)
  if (result) return result
}
</code></pre>
<p>Fuzzy matching uses Levenshtein + LCS hybrid (threshold 0.5):</p>
<pre><code class="language-typescript">fuzzyMatch('کتاب', ['کتابیں', 'کتب', 'علم'])
// → { candidate: 'کتابیں', score: ~0.7 }
</code></pre>
<hr />
<h2>Numbers — South Asian scale with bigint</h2>
<p>The South Asian number system has named units that don't exist in Western mathematics:</p>
<table>
<thead>
<tr>
<th>Urdu</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td>ہزار</td>
<td>1,000</td>
</tr>
<tr>
<td>لاکھ</td>
<td>100,000</td>
</tr>
<tr>
<td>کروڑ</td>
<td>10,000,000</td>
</tr>
<tr>
<td>ارب</td>
<td>1,000,000,000</td>
</tr>
<tr>
<td>کھرب</td>
<td>1,000,000,000,000</td>
</tr>
<tr>
<td>نیل</td>
<td>1,000,000,000,000,000</td>
</tr>
</tbody></table>
<p>The entire module uses <code>bigint</code> throughout — South Asian numbers exceed <code>Number.MAX_SAFE_INTEGER</code>.</p>
<pre><code class="language-typescript">import { numberToWords, formatCurrency, toUrduNumerals, wordsToNumber } from '@iamahsanmehmood/urdu-tools'

numberToWords(0n)                      // 'صفر'
numberToWords(100n)                    // 'ایک سو'
numberToWords(100_000n)                // 'ایک لاکھ'
numberToWords(10_000_000n)             // 'ایک کروڑ'
numberToWords(1_000_000_000_000_000n)  // 'ایک نیل'

// Ordinals with gender agreement
numberToWords(1n, { ordinal: true, gender: 'masculine' })  // 'پہلا'
numberToWords(1n, { ordinal: true, gender: 'feminine' })   // 'پہلی'
numberToWords(11n, { ordinal: true, gender: 'masculine' }) // 'گیارہواں'
numberToWords(11n, { ordinal: true, gender: 'feminine' })  // 'گیارہویں'

// Currency
formatCurrency(505.50, 'PKR')  // 'پانچ سو پانچ روپے پچاس پیسے'
formatCurrency(1000, 'INR')    // 'ایک ہزار روپے'

// Numeral conversion
toUrduNumerals(2024)    // '۲۰۲۴'

// Inverse — parse words back to number
wordsToNumber('ایک کروڑ')     // 10_000_000n
wordsToNumber('پانچ سو پانچ') // 505n
</code></pre>
<hr />
<h2>Canonical Urdu sorting</h2>
<p>No database and no JavaScript runtime has native Urdu collation. The 39-letter Urdu alphabet order:</p>
<pre><code>ء ا ب پ ت ٹ ث ج چ ح خ د ڈ ذ ر ڑ ز ژ س ش ص ض ط ظ ع غ ف ق ک گ ل م ن ں و ہ ھ ی ے
</code></pre>
<pre><code class="language-typescript">import { sort, compare, sortKey } from '@iamahsanmehmood/urdu-tools'

sort(['ے', 'ا', 'ک', 'ب'])           // → ['ا', 'ب', 'ک', 'ے']
sort(['زبان', 'اردو', 'بہترین'])      // → ['اردو', 'بہترین', 'زبان']

// Use compare() as a comparator for any sorting context
['ے', 'ا', 'ک'].sort(compare)        // → ['ا', 'ک', 'ے']

// sortKey() for indexing — diacritics stripped before key generation
sortKey('پاکستان')   // '030003091102280814'
// عِلم and عَلم sort to the same position
</code></pre>
<p>In C# it implements <code>IComparer&lt;string&gt;</code> for native LINQ integration:</p>
<pre><code class="language-csharp">using UrduTools.Core.Sorting;

var words = new[] { "ے", "ا", "ک", "ب" };
var sorted = words.OrderBy(w =&gt; w, new UrduComparer()).ToList();
// ["ا", "ب", "ک", "ے"]
</code></pre>
<hr />
<h2>Unicode-aware tokenization</h2>
<p>The tokenizer handles the edge cases that matter in real Urdu text:</p>
<pre><code class="language-typescript">import { tokenize, sentences, ngrams } from '@iamahsanmehmood/urdu-tools'

tokenize('پاکستان ایک خوبصورت ملک ہے')
// → [
//   { text: 'پاکستان', type: 'urdu-word' },
//   { text: 'ایک',     type: 'urdu-word' },
//   { text: 'خوبصورت', type: 'urdu-word' },
//   { text: 'ملک',     type: 'urdu-word' },
//   { text: 'ہے',      type: 'urdu-word' },
// ]

// Sentence splitting — on ۔ (U+06D4) ؟ ! but NOT on ، or ؛
sentences('پہلا جملہ۔ دوسرا جملہ؟ تیسرا جملہ!')
// → ['پہلا جملہ', 'دوسرا جملہ', 'تیسرا جملہ']

// The tokenizer preserves ZWNJ within words —
// so joinCompounds() output is one token per compound
</code></pre>
<p>Key edge cases handled:</p>
<ul>
<li>Izafat Kasra (U+0650) at word boundaries is not treated as a split point</li>
<li>ZWNJ-bound compounds (output of <code>joinCompounds()</code>) are kept as single tokens</li>
<li>Mixed Urdu/Latin text is classified per-token</li>
</ul>
<hr />
<h2>Transliteration — 18 aspirated digraphs</h2>
<pre><code class="language-typescript">import { toRoman, fromRoman } from '@iamahsanmehmood/urdu-tools'

toRoman('پاکستان')   // 'pakistan'
toRoman('بھارت')     // 'bharat'
toRoman('چھوٹا')     // 'chhota'
</code></pre>
<p>Digraph rules (left-to-right FSM, digraph priority):</p>
<table>
<thead>
<tr>
<th>Urdu</th>
<th>Roman</th>
<th></th>
<th>Urdu</th>
<th>Roman</th>
</tr>
</thead>
<tbody><tr>
<td>بھ</td>
<td>bh</td>
<td></td>
<td>پھ</td>
<td>ph</td>
</tr>
<tr>
<td>تھ</td>
<td>th</td>
<td></td>
<td>ٹھ</td>
<td>Th</td>
</tr>
<tr>
<td>جھ</td>
<td>jh</td>
<td></td>
<td>چھ</td>
<td>chh</td>
</tr>
<tr>
<td>دھ</td>
<td>dh</td>
<td></td>
<td>ڈھ</td>
<td>Dh</td>
</tr>
<tr>
<td>کھ</td>
<td>kh</td>
<td></td>
<td>گھ</td>
<td>gh</td>
</tr>
</tbody></table>
<pre><code class="language-typescript">fromRoman('pakistan')  // → 'پاکستان' (trie-based longest-prefix match)
fromRoman('bharat')    // → 'بھارت'
</code></pre>
<hr />
<h2>InPage encoding — decoding 30 years of Urdu archives</h2>
<p>InPage was the dominant Urdu desktop publishing tool for decades. Millions of documents — newspapers, books, government archives — exist only in InPage format. The library decodes all three versions:</p>
<pre><code class="language-typescript">import { decodeInpage, detectEncoding } from '@iamahsanmehmood/urdu-tools'

// Auto-detect InPage version and decode
const result = decodeInpage(buffer, 'auto')
// result.paragraphs → string[]  (Unicode Urdu text)
// result.version   → 'v1' | 'v2' | 'v3'

// Explicit version
decodeInpage(buffer, 'v1')  // 0x04-prefix byte-pair encoding (old InPage)
decodeInpage(buffer, 'v3')  // UTF-16LE with paragraph markers

// Detect encoding from buffer alone
detectEncoding(buffer)
// → 'utf-8' | 'utf-16le' | 'windows-1256' | 'inpage-v1v2' | 'inpage-v3' | 'unknown'
</code></pre>
<hr />
<h2>String utilities</h2>
<pre><code class="language-typescript">import { reverse, truncate, wordCount, charCount,
         extractUrdu, decodeHtmlEntities } from '@iamahsanmehmood/urdu-tools'

// Reverse word order (not characters — preserves Arabic shaping)
reverse('پاکستان ہندوستان')      // → 'ہندوستان پاکستان'

// Truncate at word boundary
truncate('یہ ایک بہت لمبا جملہ ہے', 10)  // → 'یہ ایک...'

// Count grapheme clusters (correct for combining diacritics)
charCount('عِلم')   // → 3  (ع+ِ = 1 cluster, ل, م)

// Extract Urdu/Arabic segments from mixed text
extractUrdu('The word علم means knowledge')  // → ['علم']

// Decode HTML entities BEFORE normalize() — critical for TinyMCE/Quill content
decodeHtmlEntities('کتاب&amp;rsquo;خانہ')  // → 'کتاب’خانہ'
decodeHtmlEntities('علم&amp;nbsp;ہے')      // → 'علم ہے'
</code></pre>
<p>That last one (<code>decodeHtmlEntities</code>) is the fix for the TinyMCE bug mentioned at the top. Always call it before normalizing text that came from a rich text editor.</p>
<hr />
<h2>Script and character analysis</h2>
<pre><code class="language-typescript">import { isUrduChar, getScript, classifyChar, isRTL, getUrduDensity } from '@iamahsanmehmood/urdu-tools'

isUrduChar('پ')  // true  — U+067E is Urdu-specific
isUrduChar('ب')  // false — U+0628 is shared with Arabic
isUrduChar('۱')  // true  — U+06F1 Urdu numeral

getScript('پاکستان')          // 'urdu'
getScript('مرحبا')             // 'arabic'
getScript('Hello پاکستان')    // 'mixed'

classifyChar('پ')   // 'urdu-letter'
classifyChar('َ')   // 'diacritic'
classifyChar('۱')   // 'numeral'

isRTL('پاکستان')               // true
getUrduDensity('پاکستان زندہ') // 0.28
</code></pre>
<hr />
<h2>C#/.NET — identical API, zero dependencies</h2>
<p>Every function is available in <code>UrduTools.Core</code> with the same behavior. The C# package mirrors the TypeScript structure exactly.</p>
<pre><code class="language-csharp">using UrduTools.Core.Normalization;
using UrduTools.Core.Compound;
using UrduTools.Core.Numbers;
using UrduTools.Core.Sorting;
using UrduTools.Core.Search;

// Normalize
UrduNormalizer.Normalize("عِلمٌ");                          // "علم"
UrduNormalizer.Normalize("علم‌");                      // "علم"

// Compound detection
var spans = CompoundDetector.DetectCompounds("کتاب خانہ میں");
// spans[0].Text == "کتاب خانہ"
// spans[0].Type == CompoundType.Affix

// Numbers
NumberToWords.Convert(10_000_000);  // "ایک کروڑ"
NumberToWords.Convert(1, new NumberOptions { Ordinal = true, Gender = Gender.Feminine });  // "پہلی"

// Sort
var sorted = new[] { "ے", "ا", "ک", "ب" }
    .OrderBy(w =&gt; w, new UrduComparer())
    .ToList();  // ["ا", "ب", "ک", "ے"]

// Progressive normalization for DB lookup
foreach (var form in UrduMatcher.GetAllNormalizations(userInput))
{
    var result = await db.LookupAsync(form);
    if (result is not null) return result;
}

// Match
UrduMatcher.Match("عِلمٌ", "علم").Matched;  // true, layer: StripDiacritics
</code></pre>
<hr />
<h2>Academic foundation</h2>
<p>The compound word detection module was built on peer-reviewed Urdu linguistics research. These three works directly informed the architecture:</p>
<p><strong>Jabbar, A. (2016). "Urdu Compound Words Manufacturing a State of Art."</strong>
Provides the Urdu Affix Word List (UAWL) — the definitive catalog of Urdu derivational morphemes. The 100+ affix morphemes in Layer 1 (<code>AFFIX_SET</code>, <code>PREFIX_SET</code>, <code>SUFFIX_SET</code>) are drawn from this work.</p>
<p><strong>Rahman, M. "A Linguistic Classification of Urdu Compound Words."</strong>
Informed the typological distinctions between compound categories — specifically the Perso-Arabic vs. native Urdu origin split and vav-e-atf chain patterns. Shaped the <code>CompoundType</code> taxonomy and izafat heuristics.</p>
<p><strong>"High Performance Stemming Algorithm to Handle Multi-Word Expressions."</strong>
Motivated the <code>joinCompounds()</code> + <code>tokenize()</code> pipeline design — the paper demonstrates that semantic integrity is best preserved by preventing erroneous splits at the input boundary, not by post-processing token sequences. Also reinforced N-gram scanning over bigram-only approaches.</p>
<hr />
<h2>Used in production</h2>
<p>This library is not a side project. It runs in three production systems:</p>
<table>
<thead>
<tr>
<th>System</th>
<th>Type</th>
</tr>
</thead>
<tbody><tr>
<td><a href="https://hamaariurdu.com">HamaariUrdu</a></td>
<td>Urdu language learning platform — normalization, search, compound detection, numbers</td>
</tr>
<tr>
<td><a href="https://pal.gov.pk">Pakistan Academy of Letters</a></td>
<td>Government literary institution — normalization, search, sorting</td>
</tr>
<tr>
<td><a href="https://dlp.gov.pk">Digital Library of PAL</a></td>
<td>Government digital Urdu archive — normalization, search, encoding</td>
</tr>
</tbody></table>
<p>HamaariUrdu was the origin — the library was extracted from production code where these problems were first encountered and solved. PAL and DLP integrated later for their Urdu text search and archiving systems.</p>
<hr />
<h2>Live Playground</h2>
<p>Every function is interactive at <strong><a href="https://iamahsanmehmood.github.io/urdu-tools/">iamahsanmehmood.github.io/urdu-tools</a></strong>.</p>
<p>The playground includes compound reporting built-in: if you find a compound the detector misses, or a pair it wrongly detects, you can report it directly from the UI — a pre-filled GitHub issue opens in one click.</p>
<hr />
<h2>Contributing</h2>
<p>The compound lexicon (3,262 roots, expandable) is the highest-impact area for non-developer contributions. If you know Urdu, you can contribute without writing code:</p>
<pre><code class="language-typescript">// packages/urdu-js/src/compound/lexicon-data.ts
// Format: ['rootWord', new Set(['tail1', 'tail2'])]

['محنت', new Set(['مشقت'])],
['علم', new Set(['و ہنر', 'و عمل', 'کیمیا'])],
['انسائیکلوپیڈیا', new Set(['آف اسلام'])],
</code></pre>
<p>Full guide in <a href="https://github.com/iamahsanmehmood/urdu-tools/blob/main/CONTRIBUTING.md">CONTRIBUTING.md</a>.</p>
<p>GitHub: <strong><a href="https://github.com/iamahsanmehmood/urdu-tools">github.com/iamahsanmehmood/urdu-tools</a></strong></p>
<hr />
<p><strong>اردو سافٹ ویئر کو بہتر بنانے میں ہمارا ساتھ دیں۔</strong>
<em>Help us make Urdu software better.</em></p>
<hr />
<p><em>Tags: #urdu #nlp #typescript #dotnet #opensource</em></p>
]]></content:encoded></item><item><title><![CDATA[Why I Still Use WinForms in 2026 — And When You Should Too
]]></title><description><![CDATA[Yes, WinForms. In 2026. And I'm not embarrassed about it.

I've shipped 12+ desktop applications using WinForms over the past 5 years — including a multi-terminal POS system that handles real restaura]]></description><link>https://iamahsanmehmood.hashnode.dev/why-i-still-use-winforms-in-2026-and-when-you-should-too</link><guid isPermaLink="true">https://iamahsanmehmood.hashnode.dev/why-i-still-use-winforms-in-2026-and-when-you-should-too</guid><dc:creator><![CDATA[Ahsan Mehmood]]></dc:creator><pubDate>Mon, 09 Mar 2026 18:04:26 GMT</pubDate><content:encoded><![CDATA[<pre><code class="language-markdown">
Yes, WinForms. In 2026. And I'm not embarrassed about it.

I've shipped 12+ desktop applications using WinForms over the past 5 years — including a multi-terminal POS system that handles real restaurants with real money every single day. Let me explain why WinForms still has a place, and when you should (and shouldn't) use it.

## The Hot Take

Every year, someone writes an article titled "WinForms is dead." Every year, thousands of businesses continue running on WinForms applications that just work.

Here's the reality: **WinForms isn't dead. It's boring. And boring technology that works is extremely valuable.**

## When I Choose WinForms

### 1. Internal Business Tools

When a restaurant owner needs a POS system, they don't care if it's built with Blazor, MAUI, or WinForms. They care if it:
- Launches in under 2 seconds
- Never crashes during dinner rush
- Prints receipts without drama
- Works on the old Windows 10 machine they already have

WinForms does all four of these better than any modern alternative I've tested.

### 2. Hardware Integration

Thermal printers, barcode scanners, cash drawers, kitchen display screens — WinForms talks to all of these through native Windows APIs without fighting abstraction layers.

```csharp
// Direct serial port access for receipt printer
using (var port = new SerialPort("COM3", 9600))
{
    port.Open();
    port.Write(escPosCommands, 0, escPosCommands.Length);
}
```

Try doing this in a web app. You'll need Electron, a bridge layer, custom browser extensions, or a local service. WinForms? Three lines.

### 3. Speed of Development

A WinForms CRUD app with DataGridView, forms, and dialogs takes me about **60% less time** to build than the equivalent React + API + database setup. For internal tools that 3 people will use, that speed difference is worth more than any architectural elegance.

## When I Don't Choose WinForms

### 1. Anything Cross-Platform
If it needs to run on Mac, Linux, or mobile — WinForms is wrong. I use Flutter for mobile and React for web.

### 2. Public-Facing Products
If customers will download and install it, I lean toward .NET MAUI or a web app. WinForms apps look dated by default, and while you can make them beautiful, it takes disproportionate effort.

### 3. Cloud-First Applications
If the app needs to run in the cloud, scale horizontally, or integrate with microservices — WinForms is the wrong tool. Use ASP.NET or a proper web framework.

## My WinForms Stack in 2026

```
- .NET Framework 4.8 (most projects) or .NET 8 (new projects)
- WinForms for UI
- Entity Framework for data access
- SQL Server for database
- DevExpress or custom controls for better visuals
- IMS Print Service (custom Windows Service) for printing
- MVVM-ish pattern (not strict, but organized)
```

## Real Examples

Here are WinForms apps I've shipped that are running in production right now:

| App | Users | What It Does |
|---|---|---|
| **RestoCare+ POS** | 3-5 terminals | Restaurant point-of-sale |
| **Payroll System** | 10+ users | Employee payroll processing |
| **Accounting Module** | 5+ users | Financial tracking |
| **Engineering Calculator** | 3 engineers | Structural analysis |
| **Inventory Manager** | 5+ terminals | Stock &amp; warehouse |

## The Real Lesson

Technology choice should be driven by **constraints, not trends**. My constraints are:
- Windows-only environments
- Hardware integration requirements
- Small user counts (3-50 people)
- Tight deadlines (weeks, not months)
- Reliability over aesthetics

WinForms fits these constraints perfectly. If your constraints are different, choose accordingly.

Don't let Twitter tell you what technology to use. Let your users and their problems decide.

---

*I'm Ahsan Mehmood, Co-Founder of [XechTech](https://xechtech.com). I build production software with .NET, Flutter, and AI — whatever solves the problem.*

*Connect: [LinkedIn](https://linkedin.com/in/iamahsanmehmood) · [GitHub](https://github.com/iamahsanmehmood) · [Dev.to](https://dev.to/iamahsanmehmood) · [Medium](https://medium.com/@iamahsanmehmood)*

---
</code></pre>
]]></content:encoded></item></channel></rss>