The web build has no font folder, so a text style whose TrueType file is
not a bundled Noto subset falls back to it. font_register takes the file
from the host page:
{"op":"font_register","file_name":"Brand-Regular.ttf","data_base64":"…"}
The faces join the font database, file_name resolves to the family of
the first face (case-insensitive, without directory, like installed
fonts), and open drawings lay their text out again (fallback glyph
cache cleared, text geometry invalidated per tab). The reply names the
family and the number of faces. Bytes without a face fail with
font_invalid. SysFonts::by_file becomes updatable for this.
Proposed in #1771.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.3 KiB
Driving the web build
The web build exposes the same control core as --serve and --mcp, through two
functions on the WebAssembly module. This page covers only what is specific to the
browser; the operations, request envelope and replies are the ones in the
README.
Reaching the control channel
The Trunk loader publishes the wasm-bindgen module as window.wasmBindings. The
control channel is ready once window.wasmBindings?.ocs_control_submit is defined
(the save smoke test waits for exactly that).
| Function | Behaviour |
|---|---|
ocs_control_submit(request: string) → string |
Queues one request (a JSON string) and returns a ticket immediately. |
ocs_control_take(ticket: string) → string | undefined |
Returns the reply (a JSON string) once it is ready, undefined before that. A reply is removed when taken. |
The page also loads ocs-control.js, which wraps the two functions in promises:
| API | Behaviour |
|---|---|
await window.ocsControl.ready |
Resolves once the control channel exists; the ocs-ready window event fires at the same moment. |
await window.ocsControl.request(request, {timeoutMs, pollMs}) |
Submits one request and resolves with the final reply, following accepted/running replies through op: "operation". Rejects only on timeout or a missing channel. |
Without the wrapper, poll ocs_control_take until it returns a reply, as the smoke test does:
async function control(request) {
const api = window.wasmBindings;
const ticket = api.ocs_control_submit(JSON.stringify(request));
for (let i = 0; i < 600; i++) {
const reply = api.ocs_control_take(ticket);
if (reply) return JSON.parse(reply);
await new Promise((resolve) => setTimeout(resolve, 50));
}
throw new Error('control timed out');
}
Limits: at most 64 requests wait in the queue (a further submit is answered with the
busy error), and only the newest 64 replies are kept, so take a reply soon after it is
ready. A request that is not valid JSON is answered with op: "invalid" and a
parse_error.
Opening a drawing from bytes
The web build has no file system, so open takes the file's bytes instead of a path:
{"op":"open","name":"plan.dwg","data_base64":"…"}
name is reduced to its file name and labels the tab and the recent-files entry;
data_base64 must be valid base64 and non-empty (invalid_request otherwise). While an
earlier drawing is still opening the request is answered with busy; retry it once
state shows the tab. A path is resolved against the browser's "recent files" store
rather than a disk, so use data_base64 for a file the page has not seen.
data_base64 is rejected on native builds; use path there.
Getting the drawing back
save serializes the active drawing and returns it in the reply, without a download:
{"op":"save","name":"plan.dwg","target_format":"dwg","target_version":"AC1032"}
The reply carries name, target_format, target_version, dropped_on_save, bytes
and data_base64, and the tab is marked clean. target_format and target_version
default to the drawing's own; a conversion that would drop unsupported records needs
allow_lossy: true, as on native.
Supplying fonts
The web build has no font folder. A text style whose TrueType or OpenType file is not one of the bundled Noto subsets falls back to them. The host page can hand the file over:
{"op":"font_register","file_name":"Brand-Regular.ttf","data_base64":"…"}
file_name is the name the drawing's text styles use; it is matched case-insensitively and
without a directory, as for installed fonts. The reply carries the family inside the file
and the number of faces it added. Open drawings lay their text out again. The font stays
registered for the session and is never written into saved files. Bytes without a readable
face fail with font_invalid; a missing file_name or empty or invalid base64 with
invalid_request.
Receiving saved files in the host page
A page that embeds the app can take every saved or exported file itself instead of a browser download by defining a function on the app's window before saving:
window.openCadStudioSaveFile = (name, bytes) => {
// name: file name, bytes: Uint8Array
};
While it is defined, Save, Save As, the save-before-close prompt and the exports call it with the file name and the bytes, and no file picker opens. Without it nothing changes.
Startup dialogs
A dialog can be open when the page is ready. The donation prompt appears once per
application version in each browser profile, so a fresh or disposable profile always
sees it — unless the session is automated: the first control request drops a prompt
that is still queued, and a host that sets localStorage["opencadstudio.automation"]
to "1" before the page loads never gets one. A prompt already on screen stays. While a modal is open state.modal is set, and {"op":"new"} returns ok
without creating a tab until the modal is closed. Close it first:
const state = await control({ op: 'state' });
if (state.modal) await control({ op: 'action', name: 'close_modal' });
await control({ op: 'new' });
Native builds can show a second dialog at launch (the file-association prompt), so check
state.modal again after closing one.