Files
earthtojakeandClaude Opus 5.5 cf7d992f85 One-message install, one cadgen installation, update notices, and views by absolute path (#521)
The plugin rearchitecture in one PR. It has all of the install work:
#522 (one cadgen installation) and #523 (install channels and update
notices) are folded in here and closed in favour of it, and #527 (the
animated logos) is carried too. It also has views by absolute path: the
explorer, the home and the app menu, one viewer per port, and no roots.
Rebased onto `main` at 0.7.11, after #499 and #526. It ships as a patch
release, 0.7.12.

## Before deploying

- **Run `apps/docs/src/lib/api/schema.sql` on the analytics database
before the release's docs deploy.** It only adds (a `channel` column
defaulting to `unknown`, and `source` made optional), so any time before
is fine, even now: the live receiver keeps writing meanwhile, and the
dashboard's breakdown by `source` keeps working. Until it runs,
`/v1/health` fails the deploy check.
- **OpenAI's portal acceptance of `env` in the ZIP's `.mcp.json` is
unverified.** Confirm it at the next upload. If the portal stripped it,
the OpenAI copy would only show the update button.
- **Codex tabs restored from an older CAD show "CAD was updated"** until
they are closed and CAD is opened again: the page no longer relaunches
an older build's launch (a hard cutover, like the protocol bump itself).
- **Release right after merging** (patch, 0.7.12). Until then main's
configs pin cadgen 0.7.11, which starts fine with them (tested), but it
has none of this PR's behaviour.

## Install copy: the README and the docs site

**Install opens with "Ask your agent (recommended)":** "Send this
message to your agent and it will install text-to-cad for you." The
message is `Install text-to-cad from
https://github.com/earthtojake/text-to-cad`.

**Then installing it yourself, worded for a person or their agent
alike.** Nothing is addressed to agents:
1. CAD runs through uv: check that it is installed, and install it if it
is missing.
2. Run the commands under your app's heading, or Other Agents if it has
none.
3. Restart the app.

**Every install path is a sub-section with three commands:** install,
update, and remove. To reinstall, remove it and install it again.
- Claude Code
- Claude Desktop (README only): to update, change the pinned version.
- Codex
- Cursor
- Grok Build
- Gemini, which installs with `--consent --auto-update`
- Other Agents: the Skills CLI. Its `add` is also its update and its
reinstall, and `-g -a <agent> -y` skips its questions.
- Request Plugin, which opens a new issue

On the docs page, each app's update and remove commands sit under a
collapsed **Update or reinstall**. That is where the update button's
*Manual installation* link lands: `texttocad.dev/install` redirects to
`/#install`.

**AGENTS.md now opens by pointing an agent that clones the repository to
the README's Install.**

**The plugin description drops its "Give your agent CAD superpowers."
opening everywhere it appears:** the manifests, the Claude marketplace
entry, the README intro (now directly under the title) and the docs
site's Overview. The tagline stays as the headline. Store listings pick
it up on their own schedules: Cursor when this merges, claude.ai at the
next release, OpenAI at the next ZIP upload.

**Docs site:**
- **Animated logos (#527):** the wordmark draws itself in. The README
and the hero show it as a 146 KB SVG in place of the 303 KB PNG.
- **Available for these agents**, under the hero. It scrolls skills.sh's
logo set (copied into `apps/docs/public/agents/` with approval), plus
Grok's glyph from Lobe Icons (MIT). The tiles blend into both themes,
and each logo jumps to its install. The 21 logos come to 66 KB: Nous
Research's embedded PNG went from 196 KB to 22 KB, at the size the tile
shows it.
- **Header:** the CAD wordmark is always at a phone's left; on desktop
it slides in once the hero's wordmark scrolls away.
- **Contributing** section.
- Every Copy button is the same size.
- The hero's file caption is plain text.

**One copy:**
- The site's copy lives in `src/lib/content.ts`, which the page and the
new prerendered `/llms.txt` both render.
- The README and the site share their structure, description, install
message, and every install, update and remove command.
`test_plugin_manifests.py` checks them, and AGENTS.md and
`apps/docs/README.md` state the rule.
- The README says more: numbered steps, and fuller notes under each
install.

## One cadgen installation

cadgen runs as one launch command everywhere: `uvx --no-config
--managed-python --python 3.13 --from cadgen==<version> <tool>`
(`cadgen._internal.launch`).

- **One installation and one warm daemon.** uv keeps one installation
per requirement, and cadgen names its daemon after the installation's
folder. So every app's server, every Codex thread and every skill
command share one installation and one daemon. `--managed-python
--python 3.13` makes that installation identical on every machine.
- **Server configs:** Claude's, Codex's, Cursor's and Gemini's, and the
README's Desktop snippet, run the launch command. A test holds them to
cadgen's.
- **Skills:** each cadgen skill defines `cadgen` and `python` as the
launch command. Their `requirements.txt` files are deleted, and a test
forbids any other way of installing cadgen in a skill.
- **`cadgen doctor <skill>`** reads the pin from the skill's SKILL.md
and reports the installation and daemon in use.
- **Playwright is a core dependency** (`playwright==1.63.0`); the
`snapshot` extra is gone.
  - The first snapshot installs the headless shell itself.
- On Linux, cadgen installs the browser's libraries when it has root, or
prints the exact command.
  - CI installs the headless shell only, about 350 MB less per run.
- **Release:** `sync-version.mjs` stamps every skill's launch pin, and
`check-version.sh` checks them.
- **Dev installs:** `dev_install.py` points the copied skills at the
checkout's `.venv`. `--wheel` builds the checkout's wheel and runs
everything through `uvx --from <wheel>`.

## Install channels, a daily version check and the update button

**Each plugin's startup config names where its installs come from**, in
the server's environment: `CADGEN_INSTALL_CHANNEL=<id>`. Plugins that
something else keeps up to date also set `CADGEN_AUTO_UPDATED=1`. The
viewer and daemon the server starts inherit both.

| Package | Server environment | Told of a release |
| --- | --- | --- |
| `main`'s `claude.mcp.json`: Claude Code, Grok Build, and Cursor
through Claude Code's plugins | `claude-github` | yes |
| `main`'s `codex.mcp.json` | `codex-github` | yes |
| the README's Claude Desktop config | `claude-desktop` | yes |
| the `plugin` branch's `cursor.mcp.json`, a Cursor clone
(`plugin_branch.py`) | `cursor-github` | yes |
| `main`'s `gemini-extension.json` | `gemini-github`, auto-updated | no:
Gemini updates it |
| `main`'s `cursor.mcp.json`, which the Cursor Marketplace reads |
`cursor-marketplace`, auto-updated | no: the store updates it |
| the `plugin` branch's `claude.mcp.json`, the Claude directory
(`plugin_branch.py`) | `claude-directory`, auto-updated | no: the store
updates it |
| the OpenAI ZIP (`plugin_zip.py`, replacing `--install store`) |
`openai-directory`, auto-updated | no: the store updates it |
| a dev install | `dev` | no |
| a skill's command, or the viewer a skill opens | nothing (`unknown`) |
the viewer only: a skills-only install |

- cadgen never decides by a channel's name, since its core may not name
a host (a test holds that). A new store needs only its settings, no
cadgen release.
- The analytics server lists the channel names.
- Environment variables, never flags: main's configs pin the last
release, and cadgen ignores a variable it doesn't know but refuses a
flag. Released 0.7.11 refuses `cadgen mcp --channel`, so flags would
have broken every server installed from main between merging and
releasing.

**Only a copy nothing else updates hears of a new release.**
- An auto-updated copy never checks and is never told, and neither is a
dev install.
- Every other copy reads `api.texttocad.dev/v1/versions` at most once a
day, in one anonymous request, and is told whenever it is behind
`latest`. The check never runs in CI, and `CADGEN_UPDATE_CHECK=0` turns
it off.
- The feed is only `{latest}`: the per-store minimums are gone.

**The notice** reads "A new version v0.9.0 of text-to-cad is available
(currently on v0.8.1)". Its prompt is worded like the install message:
"Update text-to-cad to 0.9.0 from
https://github.com/earthtojake/text-to-cad".
- **CAD app and browser viewer: a blue update button**
(`@text-to-cad/ui/update`): first among the navbar's right-hand
controls, and on the home a row of its own, labeled Update. Both apps
know the notice before the page draws (a launch carries it; the web page
reads it with the server's description), so it draws with the page, and
it is read again when the person comes back to the page. It opens the
update card in a popover:
- **Send to agent** (↑, as in Quick Edit) posts the prompt to the chat
where the host takes messages, with a Copy icon beside it;
  - elsewhere it is **Copy prompt**;
  - a **Manual installation** link goes to `texttocad.dev/install`.
  - Nothing dismisses the button: it stays until the install is current.
- **Inline cards are now the whole viewer** (Claude Desktop and every
other MCP Apps host): navbar with the update button, tools, view cube
and Quick Edit. Full size moves into a named slot
(`CadViewerProps.fullSize`), last in the navbar and the home's row, and
appears only where the host can show a view full size. A card used to be
a compact view of the model alone, with no navbar, so the update button
could never appear inline.
- **Text-only apps:** the line on the first `cad_show` result of each
server process. In text mode `cad_show` now returns text only, because
Claude Code shows a result's structured content in place of its text and
dropped the line.
- **Skill commands say nothing, and don't check.** The same skill files
ship in every plugin, so a command can't tell which one it came with.
The earlier once-a-day stderr line is gone.

The browser viewer's GitHub release polling and its old navbar update
button are gone. `/__cad/version` is GET only, and both apps read it
(the CAD app through its tunnel).

**The feed** is served by the docs app (`src/lib/analytics` renamed
`src/lib/api`). `latest` is the stamped version.

**Analytics** send `channel` instead of `source` (batch schema 2), and
`cadgen mcp --install` is gone. **Every released cadgen keeps
counting:** the receiver reads every schema a release sends, so
0.7.7–0.7.11 copies (schema 1) are stored with the channel `unknown` and
their `source` kept, and `schema.sql` only ever adds (a rule in
`apps/docs/README.md`).

**Nothing waits on the network** (cadgen law 19): analytics and the
version check run in the background and fail silently, and CAD works
offline once installed. The audit found one breach, now fixed:
`/__cad/version` read a due feed inside the request (up to 5 s on a
network that drops packets). The feed is now read only in the
background, as a server starts, on each launch and on each
`/__cad/version`. Checked against an installed cadgen with every request
refused, then hanging: builds, snapshots, the server's tools and the
viewer's routes all answer at once; the one wait is the exit's last
analytics send (2 s when requests hang). How cadgen is launched (uvx
re-checks its index once its cache is stale) is outside the law.

**`cad-mcp-setup` is retired, with no replacement.** Installing CAD is
uv and the app's own plugin install: the `cad` skill names uv, and the
README's section for each app carries its install, update and restart
steps, which the update button's prompt links. Store copies need nothing
run. The Codex manifest no longer names an onboarding skill. There is no
`cad_update` tool.

## Views by absolute path: the explorer, the home and the app menu

**No more roots.** A view shows one file, named by its absolute path, or
the home, and browses from that file's folder. That holds in a Codex
tab, a Claude card and the web viewer alike. Roots, the thread
workspace, MCP roots, Codex's sandbox metadata and the `explore` flag
are gone. Agents name models by absolute path only.

**The navbar:**
- **The C logo opens the app menu.** It has Back to files (the home),
the person's settings (Share anonymous usage data, Quick edit), Send
feedback, GitHub and Discord, then the version and "Made by @…". The
Settings popover is gone. The logo, the file name and the ⋯ share one
hover and the pointer, with no tooltips.
- **The file's name opens the explorer.** It shows the file's folder one
folder at a time, the last three folders as a breadcrumb with the rest
under "…", and "Filter files..." to search everything under the folder.
- **The ⋯ menu** has Copy path and Reveal.
- **The right side** is empty unless there's something to show: the
update button, a dismissed alert's icon, or Full size.
- **Display and Preview sit on top of the view cube again**, and Preview
has the Play icon again. Preview's own controls land where they sat.

**The home and missing files:**
- A view with no file shows the home: Recent Files, Open with the
desktop's chooser where one exists, and GitHub, Discord and X under the
wordmark (the home has no settings menu).
- A missing file shows "File does not exist" with Go home.
- *Open with CAD* in Codex shows its file alone, with no Back to files
and no explorer.

**Viewer (`cadgen viewer`):**
- **Absolute paths only.** Every route names files by absolute path and
nothing is scanned. The asset route still sends only CAD files and their
sidecars.
- **New routes:** the explorer has `/__cad/folder` and `/__cad/search`
(200 results, 1.5 s, 24 folders deep; hidden and build folders skipped).
The home has `/__cad/recents`, `/__cad/thumbnail` and `/__cad/pick`. A
model joins the library (`POST /__cad/recents` `open`) once it is on
screen, and is counted for analytics there.
- **`/__cad/server` says only what a reader uses:** `app`,
`identityToken`, `autoReload`, `platform` (which file manager Reveal
opens), `user`, `start`, `pick`, `port`, `pid`.
- **One viewer per port**, 3245 or `--port N`, started from anywhere:
  - a launch reuses this user's viewer when its code matches;
  - it replaces one running other code (`/__cad/shutdown`);
  - it refuses anything else.
- **Development:** `--new` takes a free port. The registry and `cadgen
viewer list` are gone.

**CAD app (`cadgen mcp`):**
- A launch is a model's absolute path or the home (protocol 5). A launch
from another build shows "CAD was updated".
- **The page has three tools: `cad_sync`, `cad_capture_reply` and
`cad_http`.** Everything else it asks of the server is the viewer's own
routes, sent over `cad_http` to one in-process viewer shared by every
view, which keeps the server's library and analytics: the models, the
library and its pictures, Open, Reveal, the analytics answer and
features, and the update check. The page's own copies of those
(`cad_consent`, `cad_features`, `cad_version`, `cad_launch`,
`cad_pick_model`, `cad_recents`, `cad_reveal`) are gone. Only the
clipboard stays the host's.
- **A text client is listed no page tool, and cannot call one:** a call
reaches only a tool this client was listed, so no agent there answers
the analytics card for the person.

**Web:** the bare URL is the home, and `?file=` takes an absolute path.
A relative one resolves where the viewer started, for development.

**Skills:** they run the viewer from any folder and link
`url?file=<absolute path>`. Copied references are absolute paths.

## Cleanup after the rearchitecture

What views by absolute path made redundant, removed:
- **Both apps reach the server through the same client.**
`@text-to-cad/core/client` has `recents`, `changeRecents`, `thumbnail`,
`keepThumbnail`, `pick`, `reveal`, `consent`, `features`, `version` and
`reportActivity`. The web app's four fetch adapters and the CAD app's
seven page tools are gone; the file menu is the shared
`createCadFileActions` in both.
- **Shared UI:** the declared-panel column (no renderer declared one),
the reload-on-change path every live renderer bypassed, the file
source's `subscribe`, `ModelLibrary`'s `pictureFrom` (the viewer
pictures cards with its own client and `onThumbnail`) and `failure`, and
dead exports. The update hook no longer reads on mount: the host hands
it the notice.
- **Core:** the client's `workspaceId` (every client had the same one),
references' `workspaceId`, and the prompt formatter's `resolvePath`
(every caller passed the identity).
- **Server:** the MCP transport's outgoing requests and notification
handler (used only for MCP roots), the viewer's duplicate pick lock (the
chooser refuses a second itself), unread server fields, and dead viewer
code (`CadgenOps.shutdown`, `STEP_DESCRIPTOR_NAME`). `/__cad/folder` and
`/__cad/search` take paths through the one absolute-path rule, so a UNC
path is refused there too.
- **Docs and wording:** "Settings" is the app menu everywhere (the
privacy policy, the README, `cad_analytics`), and no doc describes a
retired surface.

## `scripts/install/dev_install.py <host>`

Installs this checkout into `claude`, `codex`, `cursor`, `grok`,
`gemini` or `claude-desktop` as `text-to-cad@earthtojake-dev`, with its
own version per install. Running it again replaces the install;
`--uninstall` removes it. It refuses a host where another copy would
load. Tested live in each host; Codex and Grok ran in isolated homes.

## Gemini CLI extension

`gemini-extension.json` sits at the root and is stamped on each release.
Gemini installs from the latest GitHub Release's source tarball. To list
it in Gemini's gallery, add the `gemini-cli-extension` topic to the
repo.

## Also

- **CONTRIBUTING:** what each store reads, Test In Agent Apps, the
install channels table, the version feed.
- **Removed one-off scripts**, replaced by `dev_install.py` and
`--print-weights`.
- **`test.yml`** also runs on `.cursor-plugin/**`,
`gemini-extension.json` and `cursor.mcp.json`. Deploy Docs also checks
`/v1/versions`.

## Checks

- **At 1bdfa29e9 (the follow-ups and the cleanup), all pass:**
- Python: cadgen 2215 (viewer and CAD app included); skills 280, 19, 51,
77, 4, 6 and 6; global 169;
- core 1281, the core client 15; shared UI: node 746 + browser 53,
vitest 336; browser viewer 57; CAD app 34; docs 16;
- type checks for core, the shared UI, the CAD app and the browser
viewer; the dependency and kit boundaries;
  - the full bundle, `check-version.sh` and `check-builds.sh`;
- the viewer launch smoke test and the real-backend browser suite
(format, camera).
- **End to end, on this branch's wheel as cadgen 0.7.103 through the
plugins' launch line, with fresh state (43/43):**
- the launcher on spare ports (start, reuse from another folder, replace
other code, stop, refuse a port another program holds) and
`/__cad/server`'s nine fields;
- the routes by absolute path (catalog, folder, search, the asset gate,
recents, the pick guard);
- in a browser, from the wheel's own client: a STEP from a cold store
draws; the navbar's logo menu, file name and ⋯; Display and Preview over
the cube; the explorer in the file's folder, its filter, a pick that
opens the file, closes the explorer and joins the library; the app menu;
Back to files to the home and its GitHub, Discord and X; "File does not
exist"; a relative `?file=`; no page errors;
- the CAD app inline (the agent's four tools and the page's three; the
tunnel's catalog, folder, library, pictures, Reveal, analytics answer
and update check; the clipboard and shutdown refused), as Codex tabs
(home, tab, `cad_open`, the tab reopening, *Open with CAD*), and to a
text client (two tools, no page tool, a link to the model).
- **After views by absolute path (db6a5419e), all pass:**
  - shared UI: node 750 + browser 53, vitest 342;
  - core 1280, bench 24, browser viewer 57, CAD app 35;
- Python 2215, with one failure that only happens in a shell with
`PYTHONDONTWRITEBYTECODE=1` (`test_no_stale_bytecode`; it passes without
it);
  - global 168, and the dependency boundaries;
  - type checks for the shared UI, the CAD app and the browser viewer;
  - the full bundle;
  - the viewer launch smoke test (a cold surface display, reuse, stop);
  - the real-backend browser suite (format, camera).
- **By hand:**
  - in the web viewer:
- the home, the explorer and its search, "File does not exist", a
relative `?file=`;
    - the logo menu, and Display above the cube;
    - the update button and its card;
- an inline card in a local MCP Apps harness: the menu, Back to files,
the explorer;
- the launcher on a custom port: start, reuse from another folder,
refuse a port another program holds, stop.
- **Not run for views by absolute path:** Claude Desktop and Codex end
to end.

- **Full local run after the rebase onto #499 and #526, with the update
button (all pass):**
  - bundle check;
  - Python:
    - cadgen 2309;
    - skills 280, 19, 51, 77, 4, 6 and 6;
    - global 168;
  - JS:
    - core 1282;
    - bench 24;
    - shared UI: node 764 + 63, vitest 350;
    - browser viewer 61;
    - CAD app 36;
  - type checks for the shared UI, the CAD app and the browser viewer;
  - browser viewer and CAD app builds;
  - docs lint, type check and production build;
- `check-version.sh`, `sync-version.mjs --check` and `check-builds.sh`.
- **After #527 and the install copy:**
  - docs check;
  - `test-docs.sh` (16, with #527's logo tests);
  - global 168;
  - `check-builds.sh`.
- The docs page was checked in a browser: Update or reinstall at desktop
and phone width, and `/install` landing on `#install`.
- **End to end, on a simulated fresh machine:**
  - Setup:
- this branch's wheel as cadgen 0.7.99, with the plugin files stamped to
it as a release would;
    - an empty uv cache with no uv Python;
- every server started by its package's exact config, every skill
command by its SKILL.md's exact launch line.
  - 28/28 checks:
- **Cold install:** first start from a desktop app's bare PATH
downloaded uv's Python 3.13.14 and every package and answered the MCP
handshake in 7 s. A warm start took 0.2 s.
- **One installation:** the server and the skills' `python` and
`cadgen`, from a terminal PATH and a bare PATH, all use one uv
environment on uv's Python. `cadgen doctor` confirms the pin and the
kernel.
- **One daemon:** a build run by a skill (`python src/bracket.py`)
starts the daemon, and `cadgen daemon status` from either PATH shows the
same process, the one `doctor` names.
- **All 8 packages:** main's 4 configs, the README's Claude Desktop
config, the `plugin` branch's 2 and the OpenAI ZIP's. The told copies
get the notice. The auto-updated ones don't.
- **Text apps:** a store copy's `cad_show` has no line and its
server-started viewer no button; a GitHub copy has both.
- **Skill commands** print nothing and read no feed. A viewer a skill
starts shows the button.
- **Right after merge:** HEAD's real configs (pinned 0.7.11) start on
PyPI's 0.7.11.
- **Agent apps**, each in a throwaway home (real configs verified
unchanged):
- Claude Code 2.1.281: marketplace and plugin install, `claude mcp list`
shows the server ✔ Connected.
- Codex 0.153.4: marketplace and plugin install, with the server and its
channel in `codex mcp list`.
- Grok Build 1.0.40: install, then `grok mcp doctor` gives a handshake
and 14 tools.
- **Not run here:** Gemini CLI (not installed on this machine), and the
Cursor, Claude Desktop and Codex desktop apps (GUI only, or in use).
- **Nested Claude Code agents**, with stand-in CLIs and isolated config:
- Sent the update prompt, a copy installed from GitHub ran `claude
plugin marketplace update earthtojake` and `claude plugin update
text-to-cad@earthtojake`, then said to restart.
- A Claude directory copy changed nothing. The agent said the store
updates it and offered the switch.
  - An update line in a CAD result was followed the same way.
- **Earlier, against the bundled wheel run through uvx as users get it,
with a local feed:**
  - store and dev copies never asking;
  - the line on a text client's `cad_show`;
  - `cad_version` for app clients;
  - the viewer showing the analytics card, then the update button.
- **Not run:** Codex and Claude Desktop end to end, because Codex was in
use by another thread.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 22:19:15 -04:00

6.9 KiB
Raw Permalink Blame History

Brand assets

generate-logos.mjs draws the blue C, CAD and TEXTTOCAD SVGs, and the stacked TEXT TO CAD lockup, in apps/docs/public/brand/ from its block alphabet, and writes the same front outlines to models/branding/src/profiles.json. No fonts, raster tracing or runtime dependencies. animate-logos.mjs draws each mark's animated build beside it (see Animation). export-logos.mjs (uses the docs dependency sharp) then turns the SVGs into every other logo file:

node scripts/brand/generate-logos.mjs
node scripts/brand/animate-logos.mjs
node scripts/brand/export-logos.mjs

The export writes transparent 512px PNGs beside the vectors, the PNG and multi-resolution ICO favicons for the docs site and the web viewer, the viewer's CAD wordmark (packages/ui/src/assets/logo-cad.svg, the navbar's home mark and the home page's wordmark in both apps) and the Codex plugin logo (.codex-plugin/logo.png). Ordinary builds consume the committed files.

logoSvg(text, options) also accepts a face palette, edge color and width, cShape (original, spine, arms or bold) and finish: 'flat' for design studies, plus lighter, the letter indices drawn in the lighter blue, and words with stacked for the lockup (see Stacked lockup).

Letters

All letters are three units wide, four tall, extruded two units deep, and set half a unit apart, so each letter's extrusion tucks behind the next. Most bodies use half-unit blocks. T uses quarter-unit horizontal alignment to center its slimmer 1.5-unit stem. C has a 1.5-unit spine and 1.5-unit arms, identically in the square icon and both wordmarks; the standalone SVG centers it in a square viewBox without distorting it.

A’s two outer top corners and D’s two outer right corners have half-unit straight chamfers; O has them on all four corners. C retains square corners. A, D and O share one square opening, one unit on a side and centered on the letter (y 1.5–2.5); A’s foot opening is one unit wide and half a unit tall. E has three full-width, one-unit horizontal bars and a 1.5-unit spine.

X is a solid body with a 45° V-notch cut into each side, mirrored across both axes: the top and bottom notches are half a unit deep, the side notches 0.75. It is the one letter off the half-unit grid. No letter uses fillets or curves, and coplanar faces are merged, so there are no decorative grid lines.

Projection and color

The art uses an oblique projection: the front face stays upright and depth recedes at 45 degrees, shortened to 0.42 screen units per model unit. This is not a strict equal-axis isometric projection. The solids are blue: front #249ddd, top #62b7ec, side #1475ad, with thin dark-blue STEP-style edges (#123e59, 0.038 model units). TEXTTOCAD draws "TO" in a lighter blue: front #8fd3f5, top #bce5f9, side #4b9fcd, with the same edges; its sides shade toward the brand blue rather than black, so it reads as the same material in a lighter tint.

The Soft relief finish adds gentle face gradients, fine inset highlights and a subtle contact shadow. Export bounds include the shadow; the letter profiles stay unchanged. Transparent backgrounds work in both the light and dark UI.

CAD models

The generator's models/branding/src/profiles.json holds the same front outlines in block units. Rebuild the matching STEP models with the entrypoints in models/branding/src/README.md. Each block is 10 mm and the extrusion is 20 mm. CAD stores the solid geometry and the two blues; Soft relief is a rendering treatment.

Stacked lockup

logo-texttocad-stacked.svg sets TEXT TO CAD as a film title does: TEXT, then a half-size TO between two long rules, then CAD scaled up to the same width as TEXT. The small letters keep their proportions, two units of extrusion becoming one, and are tracked half a unit apart; the rules are half a unit thick and stop three quarters of a unit short of the TO. TO and its rules are the lighter blue. Each row sits a third of a unit below the one above, plus its own receding top faces. Scaled pieces keep the 0.038 edge width of the rest.

Animation

logo-c-animated.svg, logo-cad-animated.svg, logo-texttocad-animated.svg and logo-texttocad-stacked-animated.svg build each mark the way its CAD model is made, from the same outlines:

  1. Sketch. A half-unit grid and dashed construction lines at each row's cap height and baseline fade in. A pen stroke draws each piece's sketch on the grid, dropping a sketch point at every corner, and the closed profile shades. Every letter but T starts as its 3 × 4 block, A, D and O with square corners; T is sketched as itself, since its shoulders are its silhouette. The rules draw outward from the TO.
  2. Extrude. The profile sweeps back to full depth as a translucent preview body, which turns solid when it gets there; the sketch is consumed. From here on every piece is lit as the finished mark: its Soft relief gradients, inset highlights and contact shadow (the static generator's own lightFaces and shadowFilter).
  3. Cut. Every opening is sketched on the front face in the edge navy, then cut: C's and E's slots, A's foot and X's four notches (each a run of the outline that leaves one side of the block and returns to it) and the counters of A, D and O. The part is the cut profile in front of the cut depth and the whole block behind it, so the floor sinks, the walls grow and the slots open through the sides until the cut goes through the back.
  4. Chamfer. The corner edges are picked in amber, and half-unit 45° chamfers grow from them in an amber preview that switches off when they commit.
  5. Finish. The grid fades out and the static artwork takes over from the build, which already matches it pixel for pixel (the finished letters draw their faces in the static art's order), so the lighting never changes at the end.

Each word runs the steps in turn, starting 0.45 s after the word before it, so the words of TEXTTOCAD overlap; within a step, letters start a beat apart, left to right. The builds take about 3.0 s (C), 4.0 s (CAD) and 4.9 s (TEXTTOCAD and the lockup) and hold their last frame. They are SMIL, so they play inside an <img> with no script. Every animated attribute's own value is its last frame, and every layer but the final artwork starts hidden, so a renderer without SMIL shows the static mark. animate-logos.test.mjs checks those properties, that path morphs keep one command structure, and that the committed files match the generator. Pages that show a build serve the static mark under prefers-reduced-motion (a <picture> source); a fresh URL replays it.

The repository README shows logo-texttocad-animated.svg too: about 7 KB in Git, where a GIF of the same build is about 2 MB that Git cannot compress.

Loading icon

The animated loading mark is separate from these vectors: changing them must not replace its images. See packages/ui/src/assets/README.md for its provenance.