Canvas spike: Ketcher behind the adapter¶
Roadmap work package 0.3 asks whether Ketcher can be the canvas engine, measured against the criteria in section 2.2: bundle size and load time with the Indigo WebAssembly toolkit, how far the toolbar and the template library can be replaced, KET round-trip fidelity, the cost of hosting a React component in our shell, and the licence files as shipped. This page records what was built, what was measured and the recommendation.
Recommendation: go, with Ketcher 3.18.0 and its separate-WebAssembly, no-render standalone build, under the conditions at the end of this page. Two licensing actions are needed before the first distribution of the app; they are the maintainer’s decisions.
What was built¶
Piece |
Where |
What it does |
|---|---|---|
Adapter interface |
|
The only contract the shell knows: mount, KET in and out, MOL and SMILES export, layout, tool selection, undo, redo, change events. |
Ketcher adapter |
|
Mounts Ketcher’s React editor into a host element, hides the toolbars the shell replaces, adds one custom button, maps shell tools onto the engine. |
Custom template |
|
Cubane, seeded into Ketcher’s user template library and inserted by a custom button. |
Shell toolbar |
|
The shell’s own palette (select, erase, C, N, O, four bond types, charge, text, arrow, plus, cubane, undo, redo, layout, clear), driving the engine only through the adapter. |
Spike harness |
|
Mounts the engine with its own toolbars, times the load, and checks the KET round trip of a fixture reaction. |
Boundary test |
|
Fails when anything outside |
Licence check |
|
Fails CI when a package the web or desktop shell redistributes is not permissive. |
To repeat the measurements:
.venv/bin/vibe-sketch serve
cd web && npm ci && npm run build && npm run preview
Then open http://localhost:4173/spike.html and press “Round-trip fixture”.
The report panel lists the timings, the fetched files and every round-trip
check; window.vibesketchSpike exposes the same from the console.
Criterion 1: bundle size and load time¶
Ketcher’s standalone package offers four builds. The spike measured two and adopted a third:
Build |
Editor JavaScript (gzip) |
Indigo WebAssembly (gzip) |
Notes |
|---|---|---|---|
Default ( |
8.5 MB, one 28.9 MB chunk |
inlined in that chunk as base64 |
Everything on the critical path before the editor appears. |
Separate WebAssembly, render ( |
1.4 MB |
3.8 MB, an 11.8 MB |
Indigo loads in a module worker, after the editor. |
Separate WebAssembly, no render ( |
1.4 MB |
2.0 MB, a 7.5 MB |
As above, without Indigo’s image renderer and the C libraries it links. |
The 1.4 MB of editor JavaScript is Ketcher with React, MUI and lodash (1.2 MB in one chunk) plus 0.2 MB of shared chunks and CSS; the shell itself is under 10 kB. A further 1.2 MB chunk, Ketcher’s 3D viewer (Miew and three.js), is split off and loads only when that viewer opens, which the shell never does.
Load times, measured in the Chromium-based browser pane of the desktop app
against vite preview on the same machine, with the editor ready when
Ketcher calls onInit and the toolkit ready when Indigo’s worker reports
itself loaded:
Load |
Editor ready |
Toolkit ready |
|---|---|---|
Default build, first load |
0.93 s |
1.01 s |
Separate WebAssembly, first load after a rebuild |
0.45 to 1.7 s |
0.49 to 1.8 s |
Separate WebAssembly, cached |
0.14 to 0.74 s |
0.21 to 0.76 s |
The spread comes from the browser pane rather than the build: render and no-render builds measured side by side were within 40 ms of each other. The toolkit is ready 20 to 100 ms after the editor in every run, so drawing never waits for Indigo in practice.
These are local numbers, which is what the desktop app will see: Electron loads
the shell from the server on the same machine (roadmap 3.4). Over a network the transfer
dominates. At 10 Mbit/s the adopted build needs about 1.1 s for the editor’s
JavaScript and 1.6 s more for the WebAssembly, against about 7 s before the
default build shows anything. The server should send the .wasm compressed
and with long cache lifetimes; its file name carries a content hash.
Criterion 2: toolbar and template library¶
The toolbar can be replaced completely. Three mechanisms combine:
Ketcher’s
buttonsoption hides named buttons. It covers about fifty buttons (layout, clean, aromatise, the 3D viewer, settings, help, arrows, shapes, S-groups and more) but not the atom, bond, selection and template palettes.Ketcher’s four toolbars carry stable test identifiers (
top-toolbar,left-toolbar,right-toolbar,bottom-toolbar). Hiding them with CSS scoped to the canvas host leaves a full-size canvas that still renders and edits. This is the shell chrome the adapter mounts by default.The shell selects tools through the editor’s
tool(name, options)call, the one Ketcher’s own buttons make, and uses the publicundo,redo,clear,layoutand KET calls for the rest.tool()is not on ketcher-core’s publicEditorinterface; the adapter declares the member it uses and checks for it at run time, so an upgrade that drops it fails loudly. Ketcher’s keyboard shortcuts keep working in shell chrome.
Verified in the browser: the shell’s bond tool draws bonds, Undo removes them, and the Cubane button adds a cubane.
Ketcher’s customButtons option adds buttons to its own top toolbar; a
press arrives as a CUSTOM_BUTTON_PRESSED event on ketcher.eventBus. The
spike’s cubane button uses it.
The template library can be extended, not replaced. Its stock templates
are compiled into ketcher-react with no public API to add or remove groups.
What Ketcher does read at start-up is the browser’s ketcher-tmpls storage
key, where “Save as template” keeps user templates as KET. Seeding that key
before mounting puts a template into the library under “User Templates”; the
spike’s cubane appears there and places on the canvas. Replacing the stock
library would mean a fork or our own template panel driven through
addFragment, which the shell toolbar already demonstrates.
Criterion 3: KET round trip¶
The fixture (web/src/spike/fixture.ts) is one reaction carrying every
feature the roadmap names, written as KET by hand from ketcher-core’s
serializer rather than produced by the engine:
(R)-2-butanol + NH4+ → HO-[CH2-CH2]n-OH, with “Δ, 80 °C” above the arrow
Loaded with setMolecule and read back with getKet, every feature
survives:
Feature |
Sent |
Returned |
|---|---|---|
Fragments, atoms, bonds |
3 fragments; 5, 1 and 4 atoms; 4, 0 and 3 bonds |
identical |
Stereo bond |
C–O wedge (stereo 1) |
identical |
Charge |
N +1 |
identical |
S-group bracket |
SRU over 2 atoms, subscript n, head-to-tail |
identical |
Text label |
“Δ, 80 °C” (non-ASCII) |
identical, as KET 2.0 rich text |
Arrow |
open-angle |
identical |
Plus sign |
1 |
identical |
Behaviour to design around:
Coordinates move by a constant translation; relative geometry is preserved to the last digit. The document stores the KET the engine returns, so this is harmless once loaded.
Ketcher adds an enhanced-stereo label (
stereoLabel: "abs") to every stereo centre. The adapter should keep it: it is what MOL V3000 and CDXML export need.Ketcher omits empty arrays (a single-atom fragment has no
bondskey). Readers of KET must default them.Text does not survive MOL/RXN export, as the formats cannot carry it. That is why the document embeds KET verbatim (roadmap 3.1).
Indigo refuses to guess. A repeat unit with only one bond crossing its bracket made SMILES export fail with “repeating units must have exactly two outgoing bonds” rather than write something wrong. The fixture’s first draft had that mistake; the stance matches our writers’ fail-closed rule.
Stereo agrees across engines: Indigo’s SMILES writes the centre as
C[C@@H](O)CC, and RDKit, reading Indigo’s molfile, assigns R. Phase 2’s
CIP comparison before and after 3D embedding (roadmap section 6) can rely on
the canvas and the server describing the same stereoisomer.
Criterion 4: hosting a React component in our shell¶
The cost is small and contained. The shell is plain TypeScript; the adapter
owns a React root inside the canvas host and nothing else in the shell knows
React exists. The Vite React plugin compiles the adapter’s one .tsx file.
Integration needed six fixes, all inside web/src/canvas/ or the Vite
configuration:
Friction |
Fix |
|---|---|
Ketcher’s bundle reads Node’s |
|
ketcher-core imports Node’s |
The |
The separate-WebAssembly entry points ship no types in the export map. |
A two-line declaration in |
The packages do not export |
|
Ketcher calls |
The adapter wires listeners once per instance. |
The no-render build announces readiness with a different event. |
The adapter waits for either. |
Ketcher declares React 18 or 19 as peers, but its bundled 3D viewer (miew-react) requires React 18, so the shell pins React 18.3.1 to keep the dependency tree valid.
Criterion 5: licences as shipped¶
Every package the web shell redistributes is permissive: 320 production
packages, all MIT, ISC, Apache-2.0, BSD, BlueOak-1.0.0, 0BSD or Unlicense.
CI now enforces this (scripts/check_npm_licenses.mjs, run in the web
job on the web and desktop trees). The details, per pinned version, are in
the licence page. Three findings need action:
The Ketcher npm packages ship no licence file. ketcher-core, ketcher-react and ketcher-standalone 3.18.0 declare Apache-2.0 in
package.jsonand in a header on each source file, but their tarballs contain no LICENSE or NOTICE file. Ketcher’s repository has both, and its NOTICE carries attribution notices that Apache-2.0 section 4(d) requires redistributors to reproduce. The production build also strips the source headers. So the app must ship the Apache-2.0 text and Ketcher’s NOTICE itself.Indigo’s WebAssembly links InChI 1.06, in both the render and the no-render build. InChI 1.06 is under the IUPAC/InChI-Trust Licence 1.0, whose terms follow the LGPL 2.1: a program that uses the library may be proprietary, but it must say so prominently, include the licence and the copyright notice, and either give access to the library’s source or use a mechanism that lets the user replace the library. The adopted build keeps Indigo as a separate, unmodified
.wasmfile, which is that mechanism; the notices and a source pointer (Indigo 1.46.0) still have to ship. InChI 1.07 and later are MIT; RDKit already bundles 1.07.3, so the issue is specific to Indigo’s copy and would disappear if Indigo upgraded.The render build links cairo (LGPL-2.1 or MPL-1.1) and FreeType. The adopted no-render build leaves both out, and nothing the shell uses needs them: Ketcher draws the canvas and the template previews with its own renderer, and exports render on the server (roadmap 3.3).
Ketcher’s own history: version 1 was AGPL-3.0, version 2 was relicensed under Apache-2.0, and 3.x remains Apache-2.0. Nothing from version 1 is involved.
Conditions for the go¶
Pin Ketcher 3.18.0 and React 18.3.1 exactly (tested by
tests/test_canvas_boundary.py), and treat an engine upgrade as a change that reruns this spike: the round trip, the shell chrome selectors, thetool()member and the doubleonInit.Use the separate-WebAssembly, no-render build. Never inline Indigo into the JavaScript bundle; the separate file is both the performance win and the replaceable-library mechanism the InChI licence asks for.
Before the first distribution of the browser bundle or the desktop app, ship a third-party notices file with the Apache-2.0 text, Ketcher’s NOTICE, the InChI licence and copyright with a pointer to Indigo’s source, and the licences of the other bundled packages; and name InChI in the About box. Whether to also ask EPAM to move Indigo to InChI 1.07 is the maintainer’s call.
Add a Playwright smoke in Phase 1 (WP 1.1) that drives the shell toolbar and the round trip in a headless browser, so the checks this page ran by hand run in CI.
What would turn this into a no-go: a Ketcher release that removes the
editor’s tool() call without a public replacement, or a licence change
upstream. Either is survivable because the engine sits behind the adapter and
Apache-2.0 permits pinning or forking 3.18.0.