Release planning

CadCraft 1.0 impact map

Every fix and feature lined up for 1.0, and what each one touches: which apps, and which layers of the stack.

ProjectCadCraft
Sheet0.10 impact
Items110
Date2026-11-09
51Fix
22Feature
0Spike
28Platform
0Site ops
8Sanity check
1Decision
≈ 319days in the written 0.10 plans
  • Planned 101 on the plan for the release it is filed under
  • Built 0 built and working in /lab (the dev builds). Whether the public has it is its reach: "in the lab", "Live" (the last gate) or "switched off"; see docs/REACH-LIVE.md
  • Partly built 1 part of it is built in /lab; the rest waits (see "Waits on")
  • Promised 0 the live site already says it is coming
  • Moved 0 moved here from an earlier release or wishlist
  • Found 0 bug found after a release shipped; fixed in a point release or the next one
  • Candidate 3 recommended in a spike or session log; owner to choose
  • Community please test 0 built and in /use; a Discord post gives the steps and the expected result and asks people to try it on their own devices, files or know-how
  • Decision Time 5 a decision, check, file, key, flag or go only you can give; folds into the list once answered

Built means built and working in the lab. Live means the public can use it, and is the last gate: 0 Built · Live, 0 Built · in the lab.

Sheet 1 · impact areas

Where 0.10 lands

How many items touch each app and each layer, by kind. One item can touch several areas, so the rows add up to more than the item count. Pick a row to filter the schedule below.

Impact areaFixFeatureSpikePlatformSite opsSanity checkDecisionAll
Apps
CadCraft 20 17 – 10 – 3 1 51
BimCraft 18 20 – 9 – 4 1 52
ModelCraft 19 20 – 10 – 3 1 53
MarkupCraft 12 16 – 9 – 3 1 41
TakeoffCraft 14 16 – 9 – 3 1 43
CivilCraft 21 18 – 10 – 4 1 54
DetailCraft 15 17 – 9 – 4 1 46
PartCraft 15 17 – 9 – 3 1 45
Studio – 1 – – – – – 1
SuperCad Studio – 6 – 6 – 1 1 14
Layers
Site and services 13 3 – 8 – 1 – 25
Desktop host – 1 – 1 – – – 2
App shell 5 15 – 8 – 2 1 31
App code 23 4 – 3 – – – 30
Our engines 8 – – 1 – 1 – 10
Vendor engines – – – – – – – 0
Drawing surface 3 – – – – – – 3
Help and words 2 1 – – – 1 – 4
Tests and tooling 2 1 – 2 – 3 – 8

Sheet 2 · schedule

Every fix and feature

Grouped by track. App chips carry each app’s layer colour; Studio is in ink. "Waits on" names what has to happen first; the small lines under it say who asked and where the item is written up.

Add an item Fork map (Rust and C++ forks) The roadmap

Issues sync every 10 minutes; last sync Nov 9, 2026 6:40 pm.

ItemWhatAppsLayersStatusDaysWaits onIssues
Library bounties 1
FeatureMB.1 Link to MB.1Comment on MB.1 Member offered bounties

Members offer and fund bounties for Library items (today only staff post bounties, and staff pay by hand or by a Stripe Transfer from our own balance, test mode). A member-funded bounty needs escrow: (1) the poster pays first through Stripe Checkout and the bounty goes live only once the money is received; the funds sit in our Stripe balance tagged to the bounty. (2) The winner needs a Stripe Connect Express account (the maker payout setup); awarding a claim prompts them to set one up. (3) On accept, a Transfer (separate charges and transfers, the winner is unknown at payment time) sends the reward minus our fee, after a short hold against chargebacks. (4) Refund to the poster if the deadline passes with no accepted claim, or if they withdraw before any award. (5) The item still goes into the public Library under CC0 or CC BY; the poster gets no exclusive rights. Open owner decisions: who accepts (poster, staff only for disputes: recommended), our fee, who absorbs Stripe's fee on a refund. Reuses Makers, AddonMoney (test-mode guard), LibraryBounties::payWithStripe and the bounty tax-form threshold. Builds on LIB-BOUNTY (0.9).

– Site and services Planned – Owner: who accepts, our fee, Stripe-fee refunds; lawyer sign-off on holding member money (payout, tax and refund rules) From: Owner
Render engines (Track RE) 3
FeatureRE.10 Link to RE.10Comment on RE.10 Unreal in the browser edition: we host Unreal and send the picture back

A browser cannot run Unreal (native C++, no web build) or reach one on the person's computer, so the web edition can only save the Unreal folder as a zip today (RE.5). To give it Unreal pictures, Unreal must run on a machine we provide: the browser sends the RE.2 scene package, a GPU server renders the chosen camera through Movie Render Queue, the PNG comes back to the Render dialog labelled "Drawn by Unreal Engine". Needs a render service and queue (accounts, limits, timeouts, cost per picture), a GPU host (the VPS has none), and the plug-in proven in a real Unreal first (RE.5 never ran there). Recommended first if any of this is done. Not promising soon (owner 2026-11-06). Licensing points are from memory, not checked against Epic's terms. Full write-up: docs/spikes/RE-unreal-hosted.md.

BimCraftModelCraft Candidate 15 You: is browser reach the goal; Epic EULA for hosting; who pays for the GPU host; RE.5 run in a real Unreal first From: owner (Owner) docs/spikes/RE-unreal-hosted.md
FeatureRE.11 Link to RE.11Comment on RE.11 Unreal inside Studio with no install: ship a packaged CadCraft renderer built with Unreal

Studio could draw with Unreal without the person installing it, but the Unreal Editor itself cannot be redistributed as far as we know (tens of GB, Epic's EULA). What could ship is our own packaged program built with Unreal that loads our scene and draws it, under Epic's royalty and attribution terms. Problems: gigabytes added to the installer; separate Windows, Mac and Linux builds; a packaged build may not read USD at runtime like the Editor, so it would load glTF and lose fidelity; native code people download, against KICKOFF rule 44 (content first). Desktop only: it does nothing for the browser edition. Not promising soon (owner 2026-11-06). Licensing points are from memory, not checked against Epic's terms. Full write-up: docs/spikes/RE-unreal-hosted.md.

BimCraftModelCraft Candidate 20 You: read Epic redistribution and royalty terms; decide against KICKOFF rule 44 From: owner (Owner) docs/spikes/RE-unreal-hosted.md
FeatureRE.12 Link to RE.12Comment on RE.12 Unreal Pixel Streaming: a live Unreal view in the browser

Unreal runs a live session on our GPU server and streams it to the page, so a person can orbit the scene in Unreal's lighting. One GPU is held per viewer, so it is by far the most expensive route; docs/0.8-PART1.md said not planned. Only after RE.10, and only if people ask for a live view rather than a rendered picture. Not promising soon (owner 2026-11-06). Licensing points are from memory, not checked against Epic's terms. Full write-up: docs/spikes/RE-unreal-hosted.md.

BimCraftModelCraft Candidate 10 RE.10; GPU cost per viewer; you From: owner (Owner) docs/spikes/RE-unreal-hosted.md
Help and tutorials 1
FeatureTUT.1 Link to TUT.1Comment on TUT.1 Tutorial pictures, wave 3: judge the unseen topics, re-shoot clipped discs, fix help text that disagrees with the app

Waves 1 and 2 put step pictures (orange ring + numbered disc, matching caption discs) on 73 of 687 tutorial topics: 268 pictures, 4 clips. Wave 3: (1) open and judge the never-skimmed topics (DetailCraft base-plates, marks-and-bom, ifc-export, shop-drawings, select-and-change, brace-gussets; CadCraft sheets-and-viewports, blocks, select-objects, insert-from-file-and-library, pictures); (2) re-shoot pictures whose disc clips or covers text (CadCraft hatch 1, MarkupCraft markups-list-and-export 2-3 and statuses-and-replies 2, PartCraft reference-geometry 3); (3) more hands-on topics only where a picture helps (2-4 shots, skip concept/reference/settings/what's-new; clips about 1 per 8 topics), toward the owner's roughly 40% mark; (4) owner decides which help-text mismatches to correct (CadCraft object-snaps, TakeoffCraft auto-count and create-items, MarkupCraft sheet-links and rotate-scale, ModelCraft rotate-scale and paint, PartCraft extrude-and-cut, relations-dimensions, reference-geometry, draft-analysis, BimCraft model-checks and edit-elements, CivilCraft parcels and Radius). Tool: tools/help-walkthrough-shots.mjs with tools/help-shots/<app>.mjs. Wave 1-2 not committed yet.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft Planned – – owner, tutorial pictures 2026-11-07
Performance roadmap — wave 1 6
PlatformPERF.1 Link to PERF.1Comment on PERF.1 HTTP/2 for getcadcraft.com: Apache runs mpm_prefork + mod_php, so every app start is 140 to 520 requests over HTTP/1.1

Measured 2026-11-08 (tools/perf/startup.mjs, localhost): a cold /use start fetches cadcraft 398 requests / 2.6 MB (233 JS modules), bimcraft 521 / 3.8 MB, partcraft 371 / 3.2 MB, civilcraft 313, modelcraft 238, detailcraft 185, markupcraft 159, takeoffcraft 141. All over HTTP/1.1: Apache is mpm_prefork with mod_php, and mod_http2 cannot run under prefork. A browser opens 6 connections per host, so for a visitor with a 70 ms round trip the request count alone is about 400/6 x 70 ms = 4.5 s before any module runs; on localhost the same start takes 1.2 s. The fix is a server change, not code: mpm_event + php-fpm 8.3 (installed at /usr/sbin/php-fpm8.3, not in use) + Protocols h2 http/1.1. The VPS serves about 80 virtual hosts from the same Apache, every one on mod_php today, so this needs the owner's go, a maintenance window and a rollback plan (keep the prefork config). Expected: one connection, all modules multiplexed, start time near the localhost figure plus one RTT per import depth. Alternatives rejected: bundling the releases (add-ons import the apps' modules by URL; a bundle would run two copies), a CDN for app files (same request count). Decision needed: go / not now.

– Decision Time – –
PlatformPERF.2 Link to PERF.2Comment on PERF.2 Apps: the 532 KB language catalog inlined in every app page (65 % of the HTML, never cached) as a cached static file per build and locale

Measured 2026-11-08: /use/cadcraft is 828 KB of HTML (205 KB gzipped) and 532 KB of it is <script id="cc-lang"> (8,818 strings; cmd 83 KB, shell 82 KB, frame 32 KB, m3d 28 KB...), regenerated and sent on every page view because the page itself is dynamic. Readers: assets/js/app-shell.js, app-boot.js, app-lang-boot.js, i18n.js (one copy per app), supercadcraft.js, super-tools.js, exact-client.js; all read #cc-lang synchronously. Plan: Lang::bootTag writes a small tag and the catalog moves to a classic script /assets/apps/<slug>/<version>/lang/<locale>.boot.js?v= (or a generated file under a cache folder for the shell part) that sets window.ccLangCatalog, loaded with the week-long asset cache; the readers take window.ccLangCatalog first, #cc-lang second (so frozen releases and the browser edition keep working). Second visit: 0 bytes for strings instead of about 100 KB gzipped. Releases are frozen so the file can be written at release time (bin/release-app.php); a Discord member builds generate it on demand keyed by the lab stamp. Keep Lang::use (PHP needs the shell strings for the bar and Settings).

– Planned – –
PlatformPERF.3 Link to PERF.3Comment on PERF.3 Apps: the "Save for offline" file list (128 KB inline, 2,054 files stat-ed per page, 42 ms) fetched on demand instead

Measured 2026-11-08: every app page carries <script id="cc-offline"> (128 KB: the URL of every file the service worker would save) and PHP spends 14 ms in AppBuilds::offlineFiles plus 28 ms in tools/shell/offline-size.php (a filesize of all 2,054 files) to build it. It is read only when a person clicks Save for offline (assets/js/app-offline.js) and by help-panel.js for the listed-files check. Plan: keep key/page/bytes in the tag; move urls to a small endpoint (or a static JSON per release written by bin/release-app.php, a Discord member builds on demand) fetched when the button is pressed; cache offline_size per release (frozen) and per lab stamp for a Discord member. Saves about 15 KB gzipped per page view and 42 ms of PHP.

– Planned – –
PlatformPERF.4 Link to PERF.4Comment on PERF.4 Apps: modulepreload hints for the static import closure, so the import waterfall is one round trip deep instead of 4 to 8

Measured 2026-11-08 (static import graph from each release's entry module): cadcraft 281 modules, depth 6 (99 at depth 1, 104, 51, 21, 4, 1), 4.6 MB; bimcraft 422 modules, depth 4, 6.5 MB; partcraft 269 modules, depth 8; markupcraft 78 modules, depth 3. The browser only discovers level n+1 after level n arrives, so each level costs a round trip even with HTTP/2 (PERF.1). Plan: AppBuilds::headTags adds <link rel="modulepreload" href=...> for the entry's static closure (regex over import/export-from lines, cached per release in storage/ and per lab stamp for a Discord member builds; about 25 KB of tags for cadcraft, gzips well). Dynamic import() sites (38 in cadcraft, 97 in bimcraft) stay lazy. Measure with tools/perf/startup.mjs before and after; most valuable after PERF.1, still a win on HTTP/1.1 for the small deep levels.

– Planned – –
FixPERF.5 Link to PERF.5Comment on PERF.5 cc-dwg: writing an 8 MB drawing takes 5.4 s and 850 MB (Plans.dwg); reading 1.8 s. Target: write under 2 s

Measured 2026-11-08 (tools/perf/io-bench.mjs, Node 24): testfiles/powerline-plans/Plans.dwg, 8.2 MB, 288 objects, 50 blocks, 2 layouts: readDwg 1,834 ms; writeDwg 5,430 ms (9.0 MB out); 854 MB RSS. CPU profile of read + write: garbage collector 12 %, lz-r2004-write.js compressR2004 + best 16 % (hash-chain matcher, MAX_CHAIN 64, tokens as small arrays), writeDwg 5 %, nongraphic-write finish 3 %, buildModelParts 3 %, objects-write finish 3 %, BitWriter 3 %, bits-write bytes 4 %; the rest is spread thin. Plan: (1) compressor: flat Int32Array token store instead of [at,len,dist] arrays, skip the lazy second search when the first match is long, compare 4 bytes at once before the byte loop; (2) BitWriter: grow by doubling and write words, not bytes, where the profile shows bytes(); (3) measure allocation in objects-write/nongraphic-write finish (many small Uint8Array copies); (4) run bin/test-dwg* round trips after each step; the autosave (dwg-autosave.js writes a full DWG after edits stop) feels this directly on big drawings. Reading is already off the page thread (read-async.js); the writer is not: consider a worker for saves over 2 MB.

– Planned – –
Sanity checkPERF.6 Link to PERF.6Comment on PERF.6 Performance budget in the test suite: startup requests/bytes/time per app and DWG/IFC read-write times, logged per release

tools/perf/startup.mjs and tools/perf/io-bench.mjs (added 2026-11-08) measure a cold /use start per app and the engines on big files. Plan: run both at each release (bin/release-app.php or the release checklist), store the numbers in docs/releases/<x.y>.md's results section (rule: test results in release notes) and fail the smoke run when an app's request count or JS bytes grow more than 15 % over the last release without a note. Baseline 2026-11-08 in docs/0.9-sessions/perf-09.md. cc-ifc needs no work now: 22.5 MB opens in 76 ms, geometry 1.3 s (683k triangles), save 30 ms; 47 MB Schependomlaan (714k lines) opens in 411 ms, geometry 641 ms, save 743 ms.

– Planned – –
Performance roadmap — wave 2 12
FixPERF.7 Link to PERF.7Comment on PERF.7 ModelCraft: importing a 10.7 MB glTF freezes the page for 8 s and peaks at 932 MB of JS heap

Measured 2026-11-08 in /use with tools/perf/open-file.mjs (Chromium, software GL): MetalRoughSpheres.glb (10.7 MB, glTF sample assets) through the Import input: open 8.5 s, 8.2 s of it main-thread long tasks (6 tasks, the longest 3.0 s: nothing on the page answers), JS heap 22 MB to 932 MB at the peak, 413 MB once settled. During zoom right after, frames p95 649 ms (software GL; a graphics card will do better, the main-thread stalls will not). Where: io.js / modelcraft.js parse and build the scene on the page thread. Plan: parse glTF/OBJ/STL/PLY in a worker (the DWG reader already has read-async.js as the pattern), transfer the typed arrays, build meshes in batches with a yield between, free the decoded copies (keep one copy of each buffer: the GPU upload, not the parsed JSON + arrays + three.js clones). Target: no long task over 200 ms, heap peak under 3x the file. Wave 3 (apps-3d.md): 500k faces import in 9.7 s with 9.1 s blocking and 992 MB of heap (about 2 KB per face); the 5 M-face GLB (114 MB) crashed the renderer tab; the first edit after a 400 MB import costs 36 s and 9 GB in Node because of the whole-model snapshot (PERF wave 3 rows).

ModelCraft Planned – –
FixPERF.8 Link to PERF.8Comment on PERF.8 BimCraft: IFC geometry runs on the page thread — a 22.5 MB model blocks it for 4.3 s and peaks at 867 MB

Measured 2026-11-08 in /use (tools/perf/open-file.mjs): fixtures/showcase/bimcraft-water-treatment.ifc (22.5 MB, 115 elements, 683k triangles): the app says "Opened: 115 elements in 3.0 s", the page is quiet after 5.2 s, main-thread long tasks total 4.3 s (6 tasks, longest 1.66 s), JS heap 24 MB to 867 MB peak, 329 MB settled; zoom frames p95 500 ms on software GL. Where: lib/ifc/open-async.js moves only the STEP scan to a worker (about 0.4 s of this); ifc-model.js:107 then runs api.StreamAllMeshes on the page: profile placement, booleans, triangulation (cc-ifc geometry.js) for every element. Plan: run the geometry pass in the same worker (post meshes back as transferred Float32/Uint32 arrays per batch of elements, draw as they arrive, so the first walls show in under a second), or at least yield between elements; release the per-element intermediate data (the 867 to 329 MB drop is GC finding it). Node figures for the same file: geometry 1.3 s single-threaded, so most of the 4.3 s is the page's extra work (three.js meshes, materials, the model tree). Wave 3 (apps-3d.md): 115 MB blocks 13.9 s (longest task 5.5 s), 201 MB blocks 18.5 s (8.2 s), heap about 1 GB per 100 MB of IFC (201 MB → 1.99 GB); a 1 GB IFC would need about 10 GB and does not fit. The first click adds 5 to 12 s for the inverse index (its own wave 3 row).

BimCraft Planned – –
FixPERF.9 Link to PERF.9Comment on PERF.9 CadCraft: opening an 8 MB DWG takes 4.1 s with one 1.3 s freeze when the worker hands the drawing back

Measured 2026-11-08 in /use (tools/perf/open-file.mjs): testfiles/powerline-plans/Plans.dwg (8.2 MB, 288 objects, 50 blocks, 2 layouts): quiet after 4.1 s; the reader runs in a worker (dwg/read-async.js, 1.8 s in Node) and then ONE long task of 1,258 ms on the page (cadcraft.js:2414 after readDwgAsync: building the drawing, blocks, display lists, first regen); heap 19 MB to 174 MB (peak 250 MB). Plan: build the entity records and the display lists in the worker (structured-clone cost vs transferable arrays: measure), or chunk the page-side build with a yield every 50 ms and paint what is there; keep the first regen under 200 ms. The autosave write (dwg-save.js, PERF.5) is the other half of the same drawing's cost.

CadCraft Planned – –
PlatformPERF.10 Link to PERF.10Comment on PERF.10 Memory ceilings: 850 MB for an 8 MB DWG, 1.5 GB for a 47 MB IFC in Node; 867 to 932 MB heap peaks in the browser — a budget per format, measured in the suite

Measured 2026-11-08: Node RSS after read + write of Plans.dwg (8.2 MB) 854 MB; after open + geometry + save of IFC Schependomlaan (47 MB) 1.47 GB, the 22.5 MB showcase 1.18 GB; browser JS heap peaks: ModelCraft glTF 932 MB (10.7 MB file), BimCraft IFC 867 MB (22.5 MB), CadCraft DWG 250 MB (8.2 MB). A tab on a phone or an iPad is killed around 1 to 1.5 GB (Studio for iPad is milestone 09), and a 100 MB IFC would pass that today. Plan: tools/perf/io-bench.mjs and open-file.mjs record bytes per source MB per format at every release (PERF.6); targets: cc-dwg under 30x, cc-ifc under 20x (strings and the STEP line index kept once, not per entity), browser peak under 3x after PERF.7 to PERF.9; the apps say so before opening a file that will not fit (navigator.deviceMemory + the file's size), instead of a dead tab. Wave 3 (scale-1000x.md, apps-3d.md): cc-dwg 35 bytes of heap per file byte (76 MB → 2.6 GB), cc-ifc about 1 GB per 100 MB in the browser, ModelCraft about 2 KB per face, DXF 38 bytes per file byte, LandXML 14 times the file; point clouds 3.8 GB for 100 M points with the in-memory store. These are the per-format budget numbers.

– Planned – –
PlatformPERF.11 Link to PERF.11Comment on PERF.11 Website: /sitemap.xml takes 3.1 s and 6,042 SQL queries on every crawl

Measured 2026-11-08 (bin/admin-perf.php /sitemap): 3,118 ms, 6,042 queries (2,080 ms SQL): LibraryItems::browse x40 (one page of 100 per call, 1,293 ms: COUNT + LEFT JOIN library_versions + ORDER over 13,850 items each time), then per item LibraryVersions::byId, Auth::byId and LibraryAuthors::uuidFor (2,000 each). Crawlers fetch the sitemap all day; it is 474 KB of XML. Plan: one query for the library URLs (item slug/uuid + author uuid + current version date, joined, no per-row lookups), written to a cached file (storage/cache/sitemap.xml, rebuilt by cron hourly or on library publish) and served from there; keep the dynamic parts (news, tutorials, apps) as they are. Target under 50 ms, under 10 queries.

– Planned – –
PlatformPERF.12 Link to PERF.12Comment on PERF.12 Website: /library front page runs 83 queries (250 ms) — N+1 loads and a 51 ms stats query per visit

Measured 2026-11-08 (bin/admin-perf.php /library): 251 ms, 83 queries taking 130 ms: LibraryItems::browse x2 (55 ms: COUNT + LEFT JOIN + ORDER over 13,850 items), LibraryItems::publicStats 51 ms, AddonLibrary::all 9 ms, then Auth::byId x25, LibraryItems::byId x16, LibraryFiles::byId x15 for the cards. /library/browse and the category pages share browse(). Plan: batch the card lookups (one IN (...) per table), cache publicStats and AddonLibrary::all for 10 minutes (file cache in storage/, no APCu on this PHP), index the browse ORDER columns and make the LIKE search on 4 columns a FULLTEXT index; target under 60 ms and under 15 queries.

– Planned – –
PlatformPERF.13 Link to PERF.13Comment on PERF.13 Website: /tutorials spends 290 ms of PHP parsing every help Markdown file on every request

Measured 2026-11-08 (bin/admin-perf.php /tutorials): 292 ms, only 7 queries (2 ms); the time is Help::forApp x2 and Help::common x2 (core/classes/Help.php: glob the help folder, read and parse every .md topic, the shots map) for the index page. /tutorials/{app} and the in-app help panel pay the same per request. Plan: compile each build's help once (bin/build-help.php already exists for the compiled help.js) and cache Help::forApp/common per build stamp as JSON in storage/cache (or ship the parsed index with the build), so a request reads one file. Target under 30 ms.

– Planned – –
PlatformPERF.14 Link to PERF.14Comment on PERF.14 Lab: every /lab app page walks the a Discord member folder three times (181 ms of PHP, 2,938 files stat-ed)

Measured 2026-11-08 in-process for /lab/cadcraft: 181 ms of PHP: AppBuilds::headTags 34 ms (jsFilesDeep + filemtime of every a Discord member file for the import map's ?v=), devStamp 37 ms (path|size|mtime of every a Discord member file plus the shell), offlineFiles 22 ms + offline_size 42 ms (2,938 files), Lang 20 ms; the HTML is 1,039 KB (import map 105 KB, cc-lang 590 KB). Staff and lab testers only, and the lab deliberately never caches (Clear-Site-Data), so every reload pays it. Plan: one directory walk per request shared by headTags, devStamp and offlineFiles (a small in-request cache of [path, size, mtime]); PERF.2 and PERF.3 take the inline catalog and the offline list out of the page. Target under 60 ms.

– Planned – –
PlatformPERF.16 Link to PERF.16Comment on PERF.16 Static delivery: no Brotli (gzip only), 7-day cache on files that are already cache-busted

Measured 2026-11-08: app files come back Content-Encoding: gzip only (cadcraft.js 198 KB to 61 KB); mod_brotli is loaded but not configured, and Brotli typically takes another 15 to 20 % off JS and CSS (1.4 to 1.7 MB of modules per app start). Every ?v= file carries Cache-Control: public, max-age=604800 (7 days); frozen release files and ?v=-busted shell files never change under their URL, so they can be immutable for a year (fewer revalidations on the second visit). Plan (.htaccess + Apache conf, no code): Brotli for text/css, javascript, wasm, json, svg with gzip as the fallback (or pre-compressed .br/.gz next to release files written by bin/release-app.php, served with mod_rewrite, so compression happens once, not per request); Cache-Control immutable, max-age 1 year for /assets/apps/<slug>/<x.y>/ and ?v= URLs; keep 7 days for anything without a version in its URL. Measure with tools/perf/startup.mjs (bytes on the wire).

– Planned – –
Sanity checkPERF.17 Link to PERF.17Comment on PERF.17 Frame rates on a graphics card: the VPS measures software GL only (BimCraft zoom p95 500 ms, ModelCraft 649 ms) — run the open-file tool on workstation11a

tools/perf/open-file.mjs runs Chromium with swiftshader on the VPS, so the zoom frame times it prints for the 683k-triangle IFC (p95 500 ms) and the 10.7 MB glTF (p95 649 ms) are the main thread's plus a software rasteriser, not what a person sees. Plan: run the same tool on workstation11a (tools/gpu-remote/run.sh; Chrome with the GPU) against /use, record p50/p95 frame times per app and file in docs/0.9-sessions/perf-09.md, and set the budget (p95 under 33 ms while orbiting a 1M-triangle model); only then decide on instancing, LOD or culling work in the shared renderer.

– Planned – –
PlatformPERF.18 Link to PERF.18Comment on PERF.18 CadCraft Studio start-up time: a startup log with timestamps in the desktop shell, measured on Windows and macOS

Not measurable on the VPS (Studio is a Windows/macOS/Linux desktop shell around the web apps; the page part is PERF.1 to PERF.4). KICKOFF line 196 already asks for a startup log a person can send. Plan: desktop-shell logs timestamps for process start, single-instance lock, window shown, first page request, app module running, first drawing paint; the About dialog shows the last start's total; owner measures a cold and a warm start on the Windows workstation and the Mac mini with the 0.9 build and records them in the release notes. Budget: window in under 1 s, app usable in under 3 s warm.

– Planned – –
PlatformPERF.15 Link to PERF.15Comment on PERF.15 "Save for offline" downloads 107 to 260 MB per app; 103 MB of CadCraft's 173 MB is compiled help in 52 languages

Measured 2026-11-08 (bin/offline-size.php --libs): CadCraft 0.8.2 offline set 173 MB in 2,077 files: app files 161 MB, libraries 9.6 MB (pdfjs 5.2, three 2.1), shell 2.7 MB. Of the app files, help/ is 103 MB (52 compiled help.<locale>.js of 2 to 3 MB each plus 395 .md sources) and lang/ 33 MB (55 locale files); the modules a person runs are 3.7 MB at the top level. BimCraft 0.8.1 is similar; a Discord member builds reach 260 MB; MarkupCraft 78 MB carries 11.9 MB of Tesseract OCR data. On a phone this is a long download for one language. Plan: the offline set keeps the chosen locale (its lang file and its help.<locale>.js) plus English, never all 52; help .md sources are not saved (the compiled help is what the panel reads); OCR language data per picked language; optional "all languages" switch for a shared classroom machine. AppBuilds::offlineFiles takes the locale (PERF.3 fetches the list on demand). Expected: CadCraft about 35 MB instead of 173 MB, with the same offline behaviour for that person. Test: bin/offline-size.php gains --locale and the service-worker test checks the saved copy opens the help in the picked language.

– Planned – –
Performance roadmap — wave 3 60
Sanity checkPERF.19 Link to PERF.19Comment on PERF.19 Scale: time is linear, memory is the ceiling. The 10x/100x/1000x table and where each format stops

The wave 3 scale track ran every engine on 10x and where possible 100x and 1000x files (Node 24, 12 GB heap cap; docs/0.9-sessions/perf-wave3/scale-1000x.md, tool tools/perf/scale-bench.mjs, generators tools/perf/gen/, 7.6 GB of files registered in testfiles/LARGE-FILES.md). Read time scales linearly almost everywhere (exponents: DWG read 0.93, IFC open 0.95, glTF 0.9, LandXML 1.1, LAS 0.7, GDS 0.87, PDF 1.0); the DWG writer is super-linear (1.3). What stops a file is memory and the data shape: one JS string per text file (hard stop 512 MB, practical 100 MB), one object per entity (cc-dwg 35 bytes of heap per file byte), whole-document JSON per undo step. Passes today: point clouds to 2 GB (100 M points in 59.5 s, 3.8 GB), cc-ifc index to 1.9 GB (15.9 s), GDS/OASIS to 622 MB (2.4 s), pdf.js 2,000 sheets (0.6 s), glTF 400 MB import (17 s). Stops: DXF 15 MB (crash, FX.142, now fixed), cc-dwg 76 MB needs 2.6 GB and 66 s to write, LandXML 358 MB needs 4.9 GB, ModelCraft first edit after a 400 MB import 36 s and 9 GB. This row: put the 1x and 10x lines of scale-bench into the PERF.6 budget (one line per format, fail on +15 % or any task over 5 s), plus the in-app budgets the 2D and 3D tracks set with their scripts (tools/perf/apps2d-*.mjs, apps3d-*.mjs on the registered files): CadCraft 100k entities open under 3 s, redraw p95 under 33 ms, pick under 5 ms, autosave under 500 ms; CivilCraft 500k points open under 5 s, zoom p95 under 100 ms; MarkupCraft 2,000 sheets switch p95 under 150 ms, heap under 400 MB; BimCraft heap per MB of IFC, ModelCraft heap per face, DetailCraft per member, draw calls, edit to next frame. Keep the table in the release notes results section; each format and app row below is the work.

– Our engines Planned 2 – docs/0.9-sessions/perf-wave3/
FixPERF.20 Link to PERF.20Comment on PERF.20 Scale: every text format reader holds the whole file as one JS string (DXF, LandXML, OBJ, DAE, ASCII STL, USDA, the native JSON documents)

Measured: a 1 GB DXF and a 692 MB LandXML die in TextDecoder.decode ("Cannot create a string longer than 0x1fffffe8"); a 358 MB LandXML needs 4.9 GB and 27 s; a 10 MB DXF needs 364 MB (38 bytes of heap per file byte, parsed twice through dxf-model.js:65). Sites: dxf-decode.js:35-49 and dxf.js:68-74, civilcraft.js:2485 and xml.js:27, modelcraft io/obj.js:84, and JSON.parse(await file.text()) for every .modelcraft/.detailcraft/.civilcraft document (modelcraft.js:3210). The caps that exist (CivilCraft 200 MB, ModelCraft 200 MB, OBJ 64 MB) are honest and stay until the readers stream. Change: byte-level tokenizers over Uint8Array fed from Blob.slice with lazily decoded values and Float64Array/Uint32Array output, the way lib/ifc scanStep already works. DXF first (the reader is 1,563 lines, L), then LandXML (M), OBJ and ASCII STL (S each). Target: 100 MB DXF under 1 GB of memory, 1 GB files open with a progress bar instead of an error.

CadCraftModelCraftCivilCraft App codeOur engines Planned 12 – docs/0.9-sessions/perf-wave3/
FixPERF.21 Link to PERF.21Comment on PERF.21 Scale: ModelCraft, DetailCraft, PartCraft and CivilCraft snapshot the whole document as JSON on every undo step and autosave

Measured: a 400 MB glTF (5 M faces) imports into ModelCraft in 17 s and 2.5 GB, then the FIRST edit calls model.js:2408 snap() (also :2223, :2230, :2272, :2331): 407 MB of JSON, 35.6 s, 9 GB peak, so the tab dies after a successful import. At 500k faces the snapshot is 3.1 s per edit. Same shape in DetailCraft model.js:169-199 (stringify before AND after every change), PartCraft part.js:504-517 and CivilCraft project.js:155. Change: command or diff undo (or per-definition dirty snapshots), meshes kept as typed buffers instead of base64 inside JSON, autosave payloads as Blobs of those buffers (the shell's IndexedDB store already takes Blobs). ModelCraft first. Extends PERF.7 with the 10x and 100x numbers.

ModelCraftCivilCraftDetailCraftPartCraft App code Planned 10 – docs/0.9-sessions/perf-wave3/
FixPERF.22 Link to PERF.22Comment on PERF.22 Scale: cc-dwg keeps 35 bytes of heap per file byte, clones the drawing out of the worker, and the writer is super-linear (extends PERF.5, PERF.9, PERF.10)

Measured at 10x (a 76 MB R2004 drawing, 1.4 M entities): readDwg 19.9 s and 2.6 GB after read (one object per entity and per vertex, objects-read.js:608-613 and :1105), the worker reply structuredClone adds 1.06 GB and 14.6 s (read-async.js:67), writeDwg 66 s (33 s from a fresh model), exponent 1.3. A 760 MB drawing would need about 27 GB: it does not fit anywhere. Browser ceiling today about 55 to 110 MB. Change: a struct-of-arrays entity store with lazy per-entity decode from the object map (the map and section locators are small; readDwg(bytes) at read.js:30 becomes openDwg(blob) with byte-range decoding), the worker reply as transferable typed buffers (or decode on the page from the transferred bytes), and a writer that streams sections to a FileSystemWritableFileStream. Reader L, writer M. The four apps carry byte copies of lib/dwg.

CadCraftBimCraftModelCraftCivilCraft Our engines Planned 12 – docs/0.9-sessions/perf-wave3/
FixPERF.23 Link to PERF.23Comment on PERF.23 Scale: cc-ifc copies the file for the worker and keeps every mesh in memory; save is O(lines) (extends PERF.8, PERF.10)

Measured: the index scans 1.9 GB in 15.9 s and 3.3 GB in Node (linear, typed arrays, lazy per-line parse: the healthy template for the other readers). But open-async.js:33 data.slice() doubles the bytes on the page (ceiling about 1 GB in a tab), open plus geometry plus save holds 12.8 times the file (2.57 GB at 200 MB; in the browser the heap grows about 1 GB per 100 MB of IFC: 210 MB peaked at 1.99 GB, 1 GB would need 10 GB), and save() walks every line with three chunks each (model.js:232-256, 8.8 s at 115 MB). Change: transfer the caller's buffer or let the worker read from the Blob; stream meshes to GPU buffers with an LRU instead of keeping all triangles in JS; save by coalesced byte ranges into a writable stream. Pairs with the BimCraft geometry-off-the-page-thread row (PERF.8).

BimCraftDetailCraft Our engines Planned 4 – docs/0.9-sessions/perf-wave3/
FixPERF.24 Link to PERF.24Comment on PERF.24 Scale: autosave rewrites the whole document every couple of seconds of editing, in every app (extends PERF.5)

Measured and read: CadCraft autosave writes a full DWG (dwg-autosave.js:39 saveDwg; 4.8 s today, 66 s at 10x), BimCraft the full IFC bytes (bimcraft.js:585 to model.js save), MarkupCraft the whole PDF Blob (markupcraft.js:4715; TakeoffCraft stops at 64 MB, takeoffcraft.js:4313), ModelCraft the JSON (modelcraft.js:3883). On a big file autosave is the longest task the person sees while working. Change: journal autosave, keeping the opened file's bytes or handle plus the edits map (IfcFile already holds exactly that in memory), snapshots taken in a worker from transferred buffers, and a clear cap with a message above a size. Shared piece in the shell's app-autosave.js, one snapshot function per app.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shellApp code Planned 4 – docs/0.9-sessions/perf-wave3/
FixPERF.25 Link to PERF.25Comment on PERF.25 Scale: point clouds pass 2 GB, but the octree merge is single-threaded, LAZ copies the whole file into WASM and E57 is one 512 MB buffer (extends PERF.10)

Measured with lib/pointcloud (tools/perf/gen/gen-las.mjs made 50 M and 100 M point LAS files): 212 MB/10.6 M points 5.5 s, 954 MB/50 M 36.5 s, 1.9 GB/100 M 59.5 s with 3.8 GB in the in-memory store. This is the one path that reads a 2 GB user file without holding it (file-backed Blob, 2 MB windows, OPFS/IndexedDB store, packed octree nodes, transferred worker replies). The merge step is 70 % of the time (2.2 s, 25 s, 42 s) and runs on one thread; LAZ allocates the whole file in the WASM heap (las.js:187-197 _malloc(file.size)); E57 reads one 512 MB buffer (e57.js:440). So a 2 GB LAZ or a 600 MB E57 fails where the LAS passes. Change: merge across a worker pool (subtrees are independent), LAZ chunk-table streaming through a bounded heap window, E57 section slicing.

CadCraftBimCraftCivilCraft Our engines Planned 4 – docs/0.9-sessions/perf-wave3/
PlatformPERF.26 Link to PERF.26Comment on PERF.26 Scale: text writers return one string, so exports stop at 512 MB and hold twice the output

DXF (dxf.js:1324, dxf2000.js:437), LandXML (landxml.js:227), OBJ, DAE, USDA (usda-write.js:33), GeoJSON and gcode writers each build one string and return it; a 1,000,000-entity DXF export is near the V8 string limit and the string plus its Blob is twice the output in memory. Change: writers that yield chunks into a WritableStream (createWritable on a file handle when the host offers one, else a Blob built from parts), with the same report object. Pairs with the streaming readers row.

CadCraftModelCraftCivilCraft App codeOur engines Planned 4 – docs/0.9-sessions/perf-wave3/
FixPERF.27 Link to PERF.27Comment on PERF.27 Scale: MarkupCraft reads the whole PDF twice and keeps the whole PDF per autosave; OBJ import refuses at 64 MB where the same model as GLB imports fine

Two small ones from the scale track. (1) pdf.js is healthy (2,000 sheets open in 0.6 s, lazy), but markupcraft.js:1158 and :1206 read the file twice, the second time only to count pages; and the autosave stores the whole PDF Blob each time: count pages from the first open, keep a file handle or the original bytes once. (2) ModelCraft io/obj.js:32 refuses OBJ above 64 MB (about 1.7 M faces) with an honest message while the same model as a 400 MB GLB imports in 17 s; lift the cap with the byte scanner and typed arrays from the streaming readers row.

ModelCraftMarkupCraftTakeoffCraft App code Planned 2 – docs/0.9-sessions/perf-wave3/
FixPERF.28 Link to PERF.28Comment on PERF.28 CadCraft: the redraw cache is one Path2D per color for the whole drawing, rebuilt from every entity on every change, with no culling or level of detail

Measured in /use 0.8.2 (software GL, so frame time is main-thread cost) with 10k / 100k / 1M entities (1M = the 100k drawing multiplied in the page): view.js:646-721 builds one Path2D per color for the whole space and any edit, undo, redo, layer flag or REGEN rebuilds it from scratch: 0.8 s at 100k, 33 s at 1M (move all = 35 s redraw, undo 29 s, one layer off 34 s; undo itself is a delta and takes 68 ms, the redraw is the cost). Path2D.closePath() on a multi-million-segment path costs in proportion to the path: 18 % at 100k, 45 % (73 s) at 1M. The whole drawing is stroked each frame and drawTexts loops over every text: one redraw 25 ms at extents and 58 ms zoomed in at 1M. Change: per-chunk or per-tile Path2D keyed by entity, rebuilt only for the entities an operation touched, first build sliced across frames; a grid or R-tree of bounds so only visible chunks are stroked, sub-pixel chunks skipped, an offscreen bitmap for pure pans. Also a Discord member.load draws inside the load event (view.js:81 to fit() to ensureCache(): 33 of the 37 s load at 1M). Targets for the 100k drawing: open under 3 s, redraw p95 under 33 ms.

CadCraft Drawing surface Planned 10 – docs/0.9-sessions/perf-wave3/
FixPERF.29 Link to PERF.29Comment on PERF.29 CadCraft: hit test, snap and hover pick are O(n) on every pointer move (461 ms a move at 1M entities; 40 of 43 s blocked during a pointer sweep)

view.js:538-607 plus moveCursor (cadcraft.js:925) run view.pick over every entity on every pointer move: pick 1.2 / 30 / 461 ms and snap 2.2 / 31 / 314 ms at 10k / 100k / 1M entities; a pointer sweep across the drawing is blocked 1.1 s at 100k and 40 of 43 s at 1M; 54 to 80 % of the zoom and a Discord member profiles at 1M is the pick, not the drawing. Change: the same spatial index as the redraw row (grid or R-tree of entity bounds), one pick per animation frame at most, snap candidates from the cells under the cursor. Target: pick under 5 ms at 100k.

CadCraft Drawing surface Planned 4 – docs/0.9-sessions/perf-wave3/
FixPERF.30 Link to PERF.30Comment on PERF.30 CadCraft: the Problems audit stringifies every entity after every change; DXF parses on the main thread; save ceilings at 1M entities

problems-cadcraft.js:45/52 shapeKey and auditIds cost 450 ms per edit at 100k entities (a JSON key per entity after each change): make the audit incremental over the touched ids. readDxf runs on the main thread (1.9 s at 100k; the DWG reader already has a worker, dwg/read-async.js): move the DXF reader into the same worker or stream it (the scale readers row). Save ceilings: writeDxf2000 dies at 1M entities with "Invalid string length", saveDwg takes 58 s and 4.2 GB of heap at 1M (over Chromium's default) and 6.1 s / 1.4 GB at 100k: chunked Blob output (the writers row) and the DWG writer in a worker (the autosave row, which measured the autosave write at 0.5 s for 10k and 5.8 s for 100k on the main thread).

CadCraft App code Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.31 Link to PERF.31Comment on PERF.31 CivilCraft: Surface.outline() takes 20 to 30 s per call at 1 M triangles and runs again on every zoom bucket (63 % of a 30 s open)

Measured on the 500k-point survey (survey-500k.civilcraft.json and landxml-tin-500k.xml): tin.js:633-648 builds string keys for 3 M directed edges with no memo, and planScene calls outline() on every mmBucket change, so it is 63 % of the 30 s open, 79 % of the scene phase, and wheel frames have a p95 of 13 to 36 s on both files. Change: memoize the outline per surface version and use integer or half-edge keys (about 100 ms once). The biggest single win of the 2D track. Healthy around it: elevationAt 0.25 ms, profiles 4 to 10 ms, Delaunator about 0.3 s at 500k, indexed picking (16 ms sweeps).

CivilCraft App codeOur engines Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.32 Link to PERF.32Comment on PERF.32 CivilCraft: every survey point is painted every frame (13 s a paint, 4.2 s per a Discord member frame at 500k points)

scene.js:88 to view.js:175 paints all survey points on each frame: 13 s per paint and 4.2 s per a Discord member frame at 500k points, 1.2 s at the LAS import's 106k. Contours and labels add to it: contours(1) takes 6.7 s for 10,788 lines at 500k (string keys; 20,949 lines on the LAS), and labels call measureText plus fillText per frame (a 32 s paint on the LandXML scene). Change: a cached Path2D or bitmap per zoom bucket, decimation by zoom, a grid in-view test, integer keys in the contour tracer, label layout cached per bucket. Target for 500k: open under 5 s, zoom p95 under 100 ms.

CivilCraft Drawing surface Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.33 Link to PERF.33Comment on PERF.33 CivilCraft: opening a project is one 27 to 41 s main-thread task, and every chosen .json is read and parsed whole twice

f.text() then parse then buildSurface then scene run synchronously in one task (27 to 41 s at 500k; LandXML 69 MB becomes a 1.2 GB element tree). Files over 200 MB are refused by design (civilcraft.js:2320), which is honest, but ifc-native-ui.js:112 reads and JSON.parses every chosen .json whole first to sniff it (6.8 s and 751 MB for a 240 MB file that is then refused), so every project is parsed twice; the LandXML reader cannot take the 692 MB 5 M-point file (over V8's largest string) and fromJSON caps at 2 M points. Change: a 4 KB sniff, the parse and surface build in a worker returning typed arrays by transfer, a streaming LandXML reader (the scale readers row), and progress in the status line. The LAS terrain import is the model to follow: 10.6 M points streamed in 2 MB pieces, thinned to 106k, 2.3 s, no long task (wave 2 stopped at the elevation-unit dialog; Import stays disabled until it is answered).

CivilCraft App code Planned 4 – docs/0.9-sessions/perf-wave3/
FixPERF.34 Link to PERF.34Comment on PERF.34 TakeoffCraft: the snap index is built on the main thread when a sheet opens (7 to 8 s, 394 MB on a 300,000-segment sheet)

snap.js:93 pageLinework plus SnapIndex runs at sheet open with snap on by default: a 7 to 8 s task, 9 s blocked and 394 MB on the dense 300k-segment sheet (MarkupCraft builds its equivalent in 549 ms). The grid itself is right. Change: build it in a worker from the page's operator list, or lazily on the first snap request, with the first sheet view not waiting for it.

TakeoffCraft App code Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.35 Link to PERF.35Comment on PERF.35 MarkupCraft and TakeoffCraft: a 2,000-sheet set builds every row and canvas up front, TakeoffCraft rebuilds the list on each switch, heap reaches 1.66 GB

Measured on plan-set-2000.pdf (2,000 sheets, 99 MB; pdf.js opens it in 2.1 s): MarkupCraft renderSheets (markupcraft.js:1404-1426) builds all 2,000 li plus canvases at open and calls store.onSheet(key) per sheet (sheets times markups); TakeoffCraft showSheet (takeoffcraft.js:362-380) rebuilds the entire sheet list with replaceChildren plus a pass over every mark on each switch: switch p50 345 ms and 5.2 s blocked over 20 switches at 2,000 sheets (MarkupCraft 91 ms, 1.9 s); 14,000 DOM nodes and 2,000 canvases at open; heap 855 MB, 1.66 GB after scrolling the thumbnails, 1.49 GB after Find; page.cleanup() is never called. MarkupCraft Find is sequential (73 s for 2,000 sheets, 37 ms a page) and textCache is unbounded. Change: a virtualized sheet list (rows and thumbnails only for what is visible), keyed updates on switch, page.cleanup() for pages out of view, Find in batches with a bounded text cache. Targets: switch p95 under 150 ms, heap under 400 MB. Healthy: a 300k-segment sheet pans and zooms at 16 ms frames, 10,000 markups on one sheet cost 124 ms and keep 16 ms frames, TakeoffCraft 10,000 marks in 29 ms and quantities() in 0.8 ms.

MarkupCraftTakeoffCraft App code Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.36 Link to PERF.36Comment on PERF.36 DetailCraft: every edit recomputes everything: autopilot, a quadratic clash check, a full scene of 1,925 draw calls, two JSON snapshots, linear model scans

Measured in /use 0.8.1 and in Node (tools/perf/apps3d-detailcraft.mjs, apps3d-detail-node.mjs; 2,820-member grid24x12x5 and the new 10x grid48x30x10 with 28,500 members and 100x grid60x60x40 with 285,600 members): refresh() (detailcraft.js:354) after any change runs build 151 ms, markParts 261 ms, autopilot.propose 899 ms (18 times more for 6.2 times the members), clashCheck 10.2 s (quadratic), and rebuilds the whole three.js scene with a Mesh plus an EdgesGeometry per part (1,925 draw calls for 948 parts); Model.change stringifies the state twice (model.js:165/170; 20 MB at 10x, 197 MB at 100x) and undo re-parses all of it; Tx.addHole and addConnection find by find/some (model.js:349, :376), so pasting the 10x model took 2.6 s and building the 100x file 716 s. Change: incremental build by id, patch-based undo (the whole-document snapshot row), an instanced or merged scene, id maps in the model API, clash check through a grid. Healthy: build, marks, drawings and IFC export are near linear.

DetailCraft App code Planned 6 FX.144 (the constructability hang) first, or nothing above 456 members can be measured in the browser. docs/0.9-sessions/perf-wave3/
FixPERF.37 Link to PERF.37Comment on PERF.37 BimCraft: the first click builds the inverse index on the page thread (5 to 12 s), picking raycasts every triangle, a section plane rebuilds all caps

Measured in /use 0.8.1 on 22.5 / 115 / 201 MB IFC (tools/perf/apps3d-bimcraft.mjs): the first property lookup calls ifc/model.js:173 inverse() (api.js:347-421) over the whole file: one 5.1 s task at 115 MB, 7.7 s plus properties = 12.1 s blocked at 201 MB. hitAt (viewer3d.js:833) has no bounding-volume tree although three-mesh-bvh is vendored: 47 to 133 ms per raycast at 683k triangles, 180 to 508 ms at 2.26 M, on every hover. setSection rebuilds every batch's stencil caps (updateCaps viewer3d.js:504): 1.4 s, 3.9 s, 8.0 s. Change: build the inverse index in the scan worker (open-async.js) or in idle slices after open; computeBoundsTree per batch; cache per-batch cap geometry and cull batches by the plane. Open itself: blocking 9.0, 13.9, 18.5 s with longest tasks 3.0, 5.5, 8.2 s (StreamAllMeshes api.js:312 in one synchronous loop, appendGeometry ifc-model.js:238 into JS arrays, runEdges ifc-vis.js:111 at 1.1 s per million triangles, the property index put at open): that is PERF.8, now with 10x numbers. Healthy: batching (46 to 104 draw calls), render CPU 2 to 7 ms, the tree panel (19k nodes, setTree 12 ms), idle after open.

BimCraft App codeOur engines Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.38 Link to PERF.38Comment on PERF.38 ModelCraft: edits, undo, exports, autosave and the guide card do whole-model work on the page thread (500k faces: move 3.3 s, undo 2.7 s, OBJ export 12.8 s)

Measured in /use 0.8.1 with m3d-500k-faces (tools/perf/apps3d-modelcraft.mjs): import 9.7 s (9.1 s blocking, one 5.3 s task; 50k is 3.1 s), heap 992 MB (about 2 KB per face; the 5 M-face GLB crashed the renderer tab, the 177 MB OBJ is refused by MAX_TEXT after decoding it); moving a placement 271 ms (GLB) or 3.3 s (OBJ, because every edge is drawn sharp without normals: 757k edge lines), undo 2.7 s (restored definitions rebuild their geometry), Model.change calls snap() twice with meshes as base64 (model.js:2408, mesh.js:751/1029; the snapshot row); the guide card's shells() costs 1.9 s = 16 % of the import, polled by setInterval(syncGuide, 400) (modelcraft.js:3151, firstmodel.js:29), and the MeshDef.faces getter (model.js:98) converts the whole mesh on every read; exports block the page: GLB 8.5 s, OBJ 12.8 s, DAE 3.2 s, the file text 2.7 s / 43 MB, and autosave shows as a 2.6 s long task in idle. Change: writers and snapshots in a worker, the guide card from cached counts, faces as typed arrays, OBJ normals computed once so edges are not all sharp. Healthy: picking and hover 0 ms (BoxTree; the MeshPicker tree builds in 336 ms), 90 draw calls, linear import.

ModelCraft App code Planned 4 – docs/0.9-sessions/perf-wave3/
FixPERF.39 Link to PERF.39Comment on PERF.39 PartCraft: a rebuild is one synchronous task (750 features 3.0 s, a 40 by 40 hole plate 8.9 s, every edit 1.3 to 1.6 s) and the D7.51 checkpoints save nothing

Measured in /use 0.8.1 (tools/perf/apps3d-partcraft.mjs): rebuilds are linear (4 to 8 ms per feature, 5.6 ms per hole: 50 features 0.32 s, 200 1.6 s, 750 3.0 s; plates 10x10 0.43 s, 20x20 1.5 s where the 0.5 JS died, 30x30 3.7 s, 40x40 8.9 s), but rebuild() (partcraft.js:351 into the manifold wasm) runs on the page thread as one task, so the app freezes for the whole of it, and an edit of the LAST feature with the checkpoint costs 1,475 ms against 1,352 ms for a full rebuild because prefixKeys (kernel.js:814) hashes every feature's JSON per rebuild. Change: the mesh kernel in a worker like brep-worker.js with progress and cancel, cheap prefix hashing (a per-feature hash kept on the feature), undo and redo (1.6 and 1.3 s today) from cached results. Healthy: orbit at 15 draw calls, the Parameters window 5 ms.

PartCraft App codeOur engines Planned 4 – docs/0.9-sessions/perf-wave3/
FixPERF.40 Link to PERF.40Comment on PERF.40 PartCraft: assembly endFrame finds every joint's instances by a linear search (O(n squared): 0.3 s at 20,000 instances, about 30 s at 200,000)

assembly.js:201 looks up instances with find per joint inside endFrame, so placing all is quadratic: placeAll 297 ms at 20k instances in the lab build (open plus build plus place 0.43 s, 3 draw calls thanks to the 0.9 instancing, pick 7 ms) and about 30 s at 200k by the square (the lab run of assembly-200000.partcraft-asm.json did not complete in time; the 0.8.1 release does not open 0.9 assembly files and draws a Mesh, a LineSegments and a new material per instance, view3d.js:395-425). Change: a Map from id to instance built once per frame or kept on the assembly.

PartCraft App code Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.41 Link to PERF.41Comment on PERF.41 Website: Library card lists run nine queries per card: /library/author 914 queries (897 ms), /library/browse 444 (587 ms) (extends PERF.12)

Measured with bin/admin-perf.php: LibraryItems::display() (LibraryItems.php:254-266) calls LibraryVersions::byId and LibraryAuthors::publicAuthor (Auth::byId plus LibraryAuthors::uuidFor, two statements) for every row browse() already selected; item-card.inc:16 then calls LibraryPreview::forVersion, which runs LibraryVersions::fileAccess (LibraryItems::byId again, LibraryVersions.php:524), LibraryItems::byId (LibraryPreview.php:55) and LibraryFiles::byId. None of the 115 lookup methods in the service layer except one has a per-request memo. Fix (a) S: a static per-request memo keyed by id in Auth::byId, LibraryVersions::byId, LibraryItems::byId, LibraryFiles::byId and LibraryAuthors::uuidFor, cleared by the write methods: removes about 600 of the 914 statements on the author page. (b) M: browse() selects the card columns in one statement (item, current version id, license, preview file, published_at, author name, maker flag, uuid) and display() takes the joined row; fileAccess() takes the item row it already has. Target: browse under 25 statements, author under 20.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.42 Link to PERF.42Comment on PERF.42 Website: Library browse sorts every published item on a computed key and counts them again on every page; at 100x it is seconds per page

LibraryItems.php:152-157 orders by COALESCE(v.published_at, i.updated_at) DESC after the join, so LIMIT cannot help: EXPLAIN shows all 13,846 rows read through idx_browse, joined, then a filesort (64 ms) plus SELECT COUNT(*) (30 ms) plus categoryCounts (:171, 30 ms) per request; OFFSET 9600 takes 98 ms. At 1 M items the same plan is 5 to 7 s per browse page. Fix: a stored library_items.sort_at kept by LibraryItems::refresh() and on publish (the current version's published_at, else updated_at); indexes idx_browse_sort (visibility, status, sort_at, id), idx_browse_kind (visibility, status, kind, sort_at), idx_browse_cat (visibility, status, category, sort_at); ORDER BY i.sort_at DESC, i.id DESC reads 24 rows; keyset paging (?after=sort_at|id) past page 20; the count and categoryCounts cached 60 s per filter key or kept in a counts table on publish. Note: one account owns 13,844 of 13,846 items today, so the author filter plans say nothing yet about a real multi-author Library.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.43 Link to PERF.43Comment on PERF.43 Website: Library search is LIKE '%q%' on four columns while an unused FULLTEXT index sits on exactly those columns

LibraryItems.php:200 browseWhere runs (i.name LIKE ? OR i.maker LIKE ? OR i.model LIKE ? OR i.description LIKE ?) with %q%; the schema has ft_search (name, maker, model, description) and sys.schema_unused_indexes lists it. EXPLAIN: 52 ms scanning 13,846 rows today versus 16 ms with MATCH … AGAINST ('door' IN BOOLEAN MODE) reading 1,324 rows from the index; at 1 M items about 3.7 s versus tens of ms. Fix: for queries of three or more characters use MATCH(i.name, i.maker, i.model, i.description) AGAINST (? IN BOOLEAN MODE) with +word* terms, keep LIKE for one or two character queries and as OR i.model LIKE ? for part numbers (innodb_ft_min_token_size is 3); the ngram parser later for CJK names. The same path serves /api/v1/library/items?q=.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
PlatformPERF.44 Link to PERF.44Comment on PERF.44 Website: session_activity and analytics_visitors hold 1.6 M rows and 1,015 MB (83 % of the database), 42 % bots, no retention, one table never read

Measured: session_activity 1.02 M rows / 646 MB growing 51,086 rows a day (21,675 of them is_bot = 1), analytics_visitors 597k rows / 369 MB growing 38,644 a day because a cookie-less client gets a fresh visitor id on every request; the table is written on every page view (Analytics.php:75) and read nowhere. Nothing prunes either (no DELETE in bin/ or the classes except the smoke test's own rows). The report queries full-scan the table (EXPLAIN ANALYZE: summary 1.8 s, topPages 2.4 s, each audience group 1.7 s, tryOpens 1.3 s), so the nightly warm job is roughly 60 to 80 s of scans that push the whole table through a 1 GB server-wide buffer pool these two tables alone fill. CadCraft analytics is about 1.1 inserts a second, over half of all INSERTs on this MySQL, each an fsync (two autocommits per page view). At 10x the warm job takes ten minutes and evicts every other database on the server. Fix: (1) do not store bot page views as rows (count per day per bot name in a small table) and give a visitor with no cc_vid cookie no analytics_visitors row until the second request; (2) retention for humans (owner's number; 30 days for any bot rows kept) as monthly partitions by TO_DAYS(created_at) dropped with ALTER … DROP PARTITION, or a nightly batched DELETE on idx_created; (3) a daily rollup table (day, url, country, region, city, device, os, browser, referrer host, campaign, lang, is_bot, views, people) filled for yesterday only, so the dashboard and reports read at most 365 rows per group; (4) stop writing analytics_visitors or read it somewhere; drop the unused idx_session. Keep the privacy page true. Combines the SQL and server tracks (sql-audit.md 4, server-api.md 9).

– Site and services Planned 4 Owner: how long to keep page-view rows (400 days keeps the year-over-year comparison the analytics page offers). docs/0.9-sessions/perf-wave3/
FixPERF.45 Link to PERF.45Comment on PERF.45 Website: the test suites' cleanup DELETEs are the most expensive statements on the production server (1.3 s full scans of the biggest table)

performance_schema digests over 12 days: DELETE FROM session_activity WHERE query_string LIKE ? (bin/smoke-test.php:51) ran 174 times at 1.30 s each, 225 s in all, 129 M rows examined (a full scan of the biggest table per run; the next most expensive digest on the server is COMMIT at 178 s); DELETE FROM email_log WHERE to_email LIKE ? OR (template = ? AND body_text LIKE ?) 231 runs at 43 ms scanning a mediumtext (test-resources, test-owned, test-reply-drafts, test-bidding, test-a Discord member-api); DELETE FROM download_events WHERE referrer LIKE ? 174 runs; DELETE FROM form_throttle WHERE action LIKE ? OR action LIKE ? 433 runs. These run while visitors browse. Fix: capture MAX(id) when a test starts and delete WHERE id > ? AND … afterwards (the primary-key range makes it a 100-row scan), or stamp fixture rows with a fixed visitor_id prefix that idx_visitor covers; same for email_log and download_events.

– Tests and tooling Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.46 Link to PERF.46Comment on PERF.46 Website: staff pages with uncached GROUP BY scans: /admin/library/metrics 887 ms, /admin/library 422 ms, /admin/hardware 265 ms

LibraryMetrics::report (LibraryMetrics.php:129, :151, :156) is 430 ms of the metrics page, one statement 222 ms (a 41k-row range on uq_once_a_day, a sort and COUNT(DISTINCT who)), linear in rows (2,157 new rows a day, 4 s at 10x). LibraryVersions::possibleDuplicates (LibraryVersions.php:587-604) is 383 ms per /admin/library view, a self-join over 27k versions that finds nothing today. HardwareReports::summary (HardwareReports.php:93) runs 13 GROUP BY scans of all 25,914 rows (240 ms; idx_day cannot help because every row is inside 90 days). MailLog::countsByStatus (MailLog.php:180) runs twice on /admin/mail (53 ms); RoadmapItems::groups runs seven times on /roadmap-issues with the same arguments. Fix: Cache::remember(…, 600) around report, summary and possibleDuplicates (the dashboard already works this way); better for duplicates, set duplicate_of_version_id at publish time in LibraryVersions::create (one indexed lookup on idx_item_sha / idx_source_sha) and list that; for metrics a nightly library_metrics_daily rollup or at least a covering index (day, kind, ref, action, who) and drop the redundant idx_day; call countsByStatus and groups once per request and pass the value down.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.47 Link to PERF.47Comment on PERF.47 Website: missing, redundant and dead indexes (DDL in the report)

Missing: library_versions.submission_id (LibrarySubmissions::outcome, LibrarySubmissions.php:353, full-scans 27,185 rows, 40 ms per call; 26,537 versions carry a submission id) → ADD KEY idx_submission (submission_id). audit_log.action (Audit::search/searchCount :157-184 with a.action LIKE ?, StripeApprovals::siteRecords :71, DiscordConversations::itemsForMatching :321, Broadcasts::version :463; 52,791 rows growing 1,820 a day) → ADD KEY idx_action_time (action, created_at). roadmap_items.item_key alone (RoadmapSuggestions::findItem :28, ReplyDrafts::usedItems :99 query without release, so uq_release_key cannot serve them; every /suggest review runs it) → ADD KEY idx_item_key (item_key). discord_messages author_id and posted_at (DiscordInbox.php:431, :569, :54, :58; low, reader paused) when the reader resumes. Redundant: library_metrics.idx_day (prefix of uq_once_a_day), broadcasts.idx_status (prefix of idx_due): drop both. Dead weight: session_activity.idx_session (nothing selects by session_id) and the three analytics_visitors indexes (table unread); library_items.ft_search is unused only because the code does not use it (the search row): keep. Each ALTER is an online operation on these sizes; run bin/install.php with a migration.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.48 Link to PERF.48Comment on PERF.48 Website: ordinary Library page views read every published item (publicStats, LanguageNames::inUse, makers, forUser without LIMIT, a 652 KB cache per page)

LibraryItems::publicStats (LibraryItems.php:278-295) pulls every published row with its author and version into PHP to count authors and categories: 42 ms per view of /library and /library/author. LanguageNames::inUse (LanguageNames.php:77-83) joins all items to their versions (a UNION with addon_listings) on every Library page to learn that two languages exist: 28 to 44 ms. LibraryItems::makers (:268) SELECT DISTINCT maker over all published rows per browse page (fast today, linear later). LibraryItems::forUser (:221) has no LIMIT: the system account's /library/mine loads 13,844 rows. LibraryMetrics::totals is cached, but the cache file is 652 KB of JSON decoded on every card page (that is the 367 ms "PHP" of /library/browse). Fix: publicStats as one GROUP BY or Cache::remember 600 s with the key dropped in LibraryItems::refresh; cache inUse and makers the same way (or compute inUse in the seed); forUser paged by 50 like LibrarySubmissions::forUser; metrics labels read the per-ref rows (idx_ref) for the 48 refs on the page instead of the whole totals file. At 1 M items each of these is seconds; with the fix they are constant.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.49 Link to PERF.49Comment on PERF.49 Website: every signed-in page view runs the account-menu queries (5 to 6 statements; 12 Licenses::forOrg calls on /account)

navbar.inc:36 → Organizations::listedFor → forUser (Organizations.php:181, a correlated member COUNT), then per workspace Licenses::summary → forOrg (two statements), Licenses::seatFor and StoragePlans::inForce: 5 to 6 statements and about 1.5 ms on each of the 61 pages profiled, 12 forOrg calls on /account. With one workspace per account this is small; with teams it is three statements per workspace per page. Fix: compute listedFor once per request (static memo) and keep the menu's result in $_SESSION with a 60 s stamp, invalidated by Licenses::assign/unassign/grant and Organizations::addMember/removeMember (or the touched() hook the dashboard already uses).

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
PlatformPERF.50 Link to PERF.50Comment on PERF.50 Website: the Library at 1000x (1 M items, 2.8 M versions, 10 M files): what breaks and the design that does not

Scaling the measured plans linearly from 13,846 items / 27,940 versions / 108k files / 41k metric rows: browse sort plus COUNT plus categoryCounts 64+30+30 ms become 4.6+2.2+2.2 s per page (breaks at about 100x); LIKE search 52 ms becomes 3.7 s while FULLTEXT stays tens of ms (index about 1 GB at 1 M items with descriptions: budget it); the per-card N+1 stays about 450 statements per page but 450 round trips is the floor of every page; publicStats / inUse / makers / forUser full reads 42 ms become 3 s each; possibleDuplicates 159 ms becomes 16 s per /admin/library view; library_metrics at 2 M rows a day needs the daily rollup and a 90-day raw retention; /sitemap (PERF.11) must become a sitemap index of 50k-URL files written nightly, never rendered per request; hot data 5 to 8 GB against a 1 GB server-wide buffer pool (the MySQL settings row). Fine as is: PK and unique lookups (uq_slug, uq_uuid, uq_sha256, uq_item_version), int unsigned ids, the content-addressed file store, the events feed and the notification mailer (idx_feed, idx_mail keep them O(result)). This row is the design pass that ties the Library rows together and sets the data budget before the data arrives (docs/0.9-sessions/perf-wave3/sql-audit.md 12).

– Site and services Planned 5 The Library browse/search/index rows above first. docs/0.9-sessions/perf-wave3/
FixPERF.51 Link to PERF.51Comment on PERF.51 Apps: 56 to 69 % of the JavaScript fetched at start never runs; dynamic import boundaries per command, workspace and file format (extends PERF.4)

Measured with V8 precise coverage on the current releases (tools/perf/addons-coverage.mjs): 6.4 MB (CadCraft) to 10.2 MB (BimCraft) of decoded JS per start, 363 to 492 modules, and after 10 s of idle plus a first command only 31 to 44 % of those bytes have executed (CadCraft 33 %, PartCraft 35 %, DetailCraft 44 %); interaction adds 1 to 2 %. Every file of the entry's static import closure is fetched (284/284 in CadCraft, 432/432 BimCraft). Per app 23 to 33 modules ran under 15 %: 0.3 to 1.1 MB each app, for example the whole DWG/DXF save path in CadCraft (dxf.js 95 KB at 8 %, dwg/write.js 58 KB at 8 %, dxf2000.js, lisp/*), ModelCraft io.js 106 KB at 11 %, PartCraft ws-illustrate 96 KB, sketchview 79 KB, ws-script 66 KB, BimCraft res-ui 107 KB, CivilCraft eg-ui 47 KB and the river/rail/water UIs. modulepreload (PERF.4) hides latency but does not cut bytes. Change: import() boundaries at the command, workspace and file-format level (the command registry already knows each command's module, input-<slug>.js); start with the entry, the view, the model and the toolbar; keep modulepreload for what stays static. Target: 1.5 to 3 MB less to download, parse and compile before the first drawing.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App code Planned 10 – docs/0.9-sessions/perf-wave3/
FixPERF.52 Link to PERF.52Comment on PERF.52 Apps: the add-on loader is serial and imports every add-on's help at activation: 13 add-ons on = 235 requests, 3.0 MB, 2.5 s of steps

addons.js (the shell copy synced into every build) awaits each bundled addon.json in turn (:390-396; 13 in CadCraft, default-off ones too), activates one after another (:381) and imports each add-on's help module at activation (:572-589; Electrical's 58 KB every start). Measured with 13 add-ons on (tools/perf/addons-load.mjs): 235 requests, 3.0 MB, 2,552 ms of serial steps, longest step 382 ms; in parallel the same work is about 0.4 s. Baseline CadCraft with nothing on: 13 serial addon.json round trips, about 0.9 s on a real line. Change: put every bundled manifest into addons/index.json at release time (one request) or Promise.all the discovery, import all entries in parallel and activate() in index order, import help lazily from api.help on F1.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shell Planned 3 – docs/0.9-sessions/perf-wave3/
PlatformPERF.53 Link to PERF.53Comment on PERF.53 Apps: 29.6 MB of byte-identical files under different URLs across the eight releases, and every new release re-downloads 98 % unchanged files

tools/perf/addons-shared.mjs hashed every file of the eight current releases: 665 distinct contents under 2,163 URLs, 29.6 MB extra on the wire for a person who opens more than one app (cabinetcraft add-on in four apps 5.8 MB, dwg/ 3.0 MB, ifc schemas in four apps 2.3 MB, regs in four 2.3 MB, m3d 1.3 MB, crs-db.json 914 KB three times, host.js / addons.js / libitem.js eight times). Across releases: CadCraft 0.8.1 → 0.8.2 kept 2,084 of 2,127 files byte-identical (158 MB) and every one is re-downloaded under the new version URL; the same for every app and every "Save for offline" re-save (PERF.15). Vendor libraries are fine (one URL each, pinCurrent works). Change: a shared /assets/lib/<name>/<content-hash>/ URL for lib/* copies and shared add-ons (release-app.php pins it like vendor paths; the import map aliases ./dwg/x.js to it), or content-hashed file names so an unchanged file keeps its URL across releases. Both shrink the import map, which lists every file because ?v= busting needs an entry per module (499 entries / 48 KB in CadCraft, 772 / 78 KB in BimCraft per page view; AppBuilds.php:391-397).

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft Site and servicesApp shell Planned 8 – docs/0.9-sessions/perf-wave3/
FixPERF.54 Link to PERF.54Comment on PERF.54 Website: /library/kits takes 1.3 to 1.45 s and 496 queries; /library/addons 113 queries

FieldPacks::byId() iterates all(), which re-reads and re-checks 74 pack.json files (949 KB) per call, once per kit row (FieldPacks.php:116-137, library/section.php:162): about 5,500 json_decodes per request; coverage() asks AddonLibrary::byListingId per member (405 queries, FieldPacks.php:154) though AddonLibrary::all() is already cached. /library/addons: AddonLibrary::media() once per card (section.php:24, 93 queries). Fix: memoize FieldPacks::all() per request (a static cache like AddonLibrary::$cache), byId from that map, coverage() from AddonLibrary::all()[$mid]; one SELECT … WHERE listing_pk IN (…) for media (mediaCounts() already exists for counts). Expected: kits under 150 ms and about 20 queries. Note for bin/admin-perf.php: rewritten routes need the real file path (/library/section?section=kits).

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.55 Link to PERF.55Comment on PERF.55 Apps: 84 to 95 % of the language keys shipped on an app page are never read; split the catalog by namespace (extends PERF.2)

Instrumented t() on the current releases: CadCraft ships 8,818 keys / 529 KB in #cc-lang, 442 are read by the end of idle and 1,024 after a first command; 70 namespaces (198 KB) are never touched in the first minute (frame 32 KB, m3d 29 KB …); BimCraft 117 namespaces / 300 KB untouched; PartCraft's script namespace 1,140 keys / 73 KB with 13 read; the shell's 1,515 keys / 82 KB with about 300 read in every app. Parsing is cheap (5 to 11 ms); the bytes are the cost, 400 to 500 KB of every page view. Change: split the catalog by namespace at build time (tools/lang), inline shell plus the entry's namespaces, load a namespace on the first t() miss or together with the module that owns it (the dynamic-import row gives the boundary); Lang::merged() key prefix is the split key.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft Help and wordsApp shell Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.56 Link to PERF.56Comment on PERF.56 Apps: the shell costs 154 to 257 ms of main thread per start, and three document-wide MutationObservers fire 100+ times a second while drawing

Measured per app: app-phone.js 17 to 56 ms running the phone layout on a desktop viewport; app-boot.js 10 to 73 ms with a body-subtree MutationObserver whose say() rewrites the DOM on every mutation (app-boot.js:82-83) and never settles while the pointer moves (141 to 221 boot-step rewrites in 3 s of drawing); three observers cascade (app-shell.js:477-482, app-phone.js:421 and :483; CadCraft 360 observer callbacks in 3 s of drawing); app-settings.js 15 to 65 ms; app-devices-ui.js 26 ms. Shell bytes: 1.35 MB fetched, about 780 KB never run (app-addons-account.js 59 KB when signed out, calc.js, ai-*.js, app-devices-ui.js, app-propgrid.js). Change: app-phone returns after the matchMedia check on wide pointer-fine screens and attaches its observers only when cc-phone is on; app-boot ignores mutations inside output-like elements and disconnects on settle; app-shell observes dialog additions only (or an attributeFilter without childList on the whole tree); dynamic-import addons-account, calc, ai-*, devices-ui on first use.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shell Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.57 Link to PERF.57Comment on PERF.57 Apps: start-up work for panels nobody opened: BimCraft massing 191 ms, ModelCraft swatches 182 ms plus 650 ms idle tasks, CivilCraft plants seed, exr table

BimCraft massingOptions() runs 191 ms inside mountProgram (design-ui.js:77); ModelCraft paints a swatch per library material from 128 px procedural textures at start (modelcraft.js:557, :581; 182 ms) and is the only app with long tasks during idle (650 ms); CivilCraft fetches and cleans plants-seed.json (198 KB) at start (landscape-ui.js:117); exr/util.js builds a 65,536-entry half-float table at import (exr/util.js:38-46); three.module.js top level is 65 to 94 ms. Change: compute on first open of the panel or first paint of the palette (IntersectionObserver or the tab event), fetch plants on first Landscape use, build the half-float table lazily. 0.2 to 0.4 s of blocking per start in the 3D apps.

BimCraftModelCraftCivilCraft App code Planned 2 – docs/0.9-sessions/perf-wave3/
PlatformPERF.58 Link to PERF.58Comment on PERF.58 Apps: an installed add-on package is relinked from blob modules on every start (hvac 334 ms, plant 756 ms) and ships as base64 JSON

addons.js linkPackage turns each verified package into blob: module URLs every start: hvac 0.9.0 (193 files, 1.8 MB; 2.4 MB as base64 JSON) costs 334 ms per start (link 115, import 55, verify 6); plant 756 ms (import 447). The Ed25519 plus sha256 check is cheap (6 to 11 ms for 100 to 193 files); blob modules get no HTTP cache and no V8 code cache, so the cost repeats on every visit and grows with file count. Change: keep the signature check but serve verified packages from the service worker under a stable /addons/pkg/<sha>/<path> scope (or cache the linked texts and import() a persistent URL) so modules get code caching; stop base64 in the package JSON (binary zip, about 34 % smaller). AddonPackages.php, addons.js, sw.js.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shellSite and services Planned 3 – docs/0.9-sessions/perf-wave3/
PlatformPERF.59 Link to PERF.59Comment on PERF.59 Apps: no service worker until someone clicks "Save for offline"; every repeat visit is a cold start minus the 7-day HTTP cache

app-offline.js:160 registers /sw.js only on the click, and sw.js:46 installs nothing on its own; the PWA manifest promises more than the page delivers. Every visit refetches the 830 to 940 KB app page (PERF.2, PERF.3) and relies on the HTTP cache for the 300 to 500 module files (which Vary: Referer defeats, see the server rows). Change: register /sw.js on every /use page; cache-first for immutable URLs (?v= and pinned vendor/app folders: the worker already knows the rule, sw.js:33-40), stale-while-revalidate for the page; "Save for offline" stays the explicit full copy. Pairs with the shared-URL row (a shared lib URL is cached once for all apps) and the installed-package row.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shell Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.60 Link to PERF.60Comment on PERF.60 Apps: the help index is one 1.9 MB module imported on F1 (52 locale copies = most of the 103 MB offline help; extends PERF.15)

CadCraft help/help.js is 1.9 MB (BimCraft 1.7 MB), parsed whole on the first F1; the 52 compiled locale copies at 2.1 to 2.2 MB each are most of the 103 MB help folder that "Save for offline" downloads. Change: compile help as an index (titles, search terms) plus one chunk per topic group, load a topic on open; offline saves the index for every language and topics for the chosen one. bin/build-help.php, help-panel.js.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft Help and words Planned 3 – docs/0.9-sessions/perf-wave3/
Sanity checkPERF.61 Link to PERF.61Comment on PERF.61 Apps: bundled add-ons that can never load in a release are still shipped and probed at every start

In /use CivilCraft and DetailCraft the regs add-on is refused "api-newer" (the release speaks add-on API 1.3, the add-on needs 1.4), BimCraft energyplus and CivilCraft site-geocode-osm are refused "manifest"; each release ships them (0.9 to 2.3 MB) and fetches their addon.json at every start, and Settings shows greyed entries. Change: bin/release-app.php refuses or drops a bundled add-on whose manifest fails AddonCatalog::checkManifest for the release's API. Also the Library should hide packs a release cannot run (seen in wave 2).

BimCraftCivilCraftDetailCraft Tests and tooling Planned 1 – docs/0.9-sessions/perf-wave3/
PlatformPERF.62 Link to PERF.62Comment on PERF.62 Server: the opcache interned-strings buffer is full and the cache restarts during the day

Measured: opcache interned strings 8.0 of 8.0 MB used (70,128 strings), oom_restarts 1 at 20:11 UTC today on a cache started 01:58; 1,195 scripts, 54 of 128 MB. After each restart every PHP file of every vhost recompiles (CadCraft alone 379 scripts / 33 MB): a CPU spike and slow first requests several times a day. Change in /etc/php/8.3/apache2/conf.d/10-opcache.ini: opcache.interned_strings_buffer=32, opcache.memory_consumption=256, then apache2ctl graceful. Healthy otherwise: hit rate 99.94 %, realpath cache 14 % used, autoloader one is_file per class, .env parse 0.04 ms, bootstrap 2 to 3 ms, DB connect 0.8 ms.

– Site and services Decision Time 1 Owner: server-wide PHP setting (shared by about 80 vhosts), no functional risk. docs/0.9-sessions/perf-wave3/
FixPERF.63 Link to PERF.63Comment on PERF.63 Website: a distributed crawl of /roadmap-issues filter combinations is about half of all PHP page requests (485,944 in 8 days, 172 ms and 1.4 MB of HTML each)

Access logs: 485,944 requests to /roadmap-issues in 8 days (184,651 on Nov 6); in the last 5 hours 24,548 requests from 17,356 distinct IPs with browser user agents and fake referers, every permutation of ?app=&channel=&kind=&layer=&release=; 23,831 of the last 49,776 dynamic requests. Each costs 172 ms of PHP, 23 queries and 1,416 KB of HTML (112 KB gzipped): about 23 CPU-hours and 54 GB of egress in 8 days, invisible on /admin/analytics because Analytics::SKIP_PREFIXES has /roadmap-issues. The crawl ignores robots, so per-IP throttling cannot help. Change: render the item list once per release into Cache (60 s) and filter from it (172 to about 20 ms); rel="nofollow" on every filter link and a canonical URL per release; if the page stays reachable by link, filter client-side on the cached list. Removes about 45 % of PHP CPU and most HTML egress today; the page gets 8x cheaper for staff.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.64 Link to PERF.64Comment on PERF.64 Website: a session file is written for every anonymous page view (312,607 files, 1.3 GB, 92,632 a day, GC 38 s)

sessions/ holds 312,607 files, 98.8 % under 120 bytes (a CSRF token or nothing), 92,632 written in 24 h and still 7,574 an hour after today's crawler change; the directory inode is 142 MB (a directory never shrinks); session-gc takes 38 s every 6 h (38,589 CSRF-only files removed per run). Every page view is open, flock and write of a file; the per-session lock also serializes one visitor's parallel requests. (FX.136 shortened the lifetime; this removes the cause.) Change: start the session lazily, only when a cc_session cookie arrives or something is written (csrf_token(), flash(), sign-in); a signed double-submit CSRF token needs no session for visitors; then recreate sessions/ once. About 90k fewer file writes a day and GC in a second.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.65 Link to PERF.65Comment on PERF.65 Website: Vary: Referer on every response, assets included, so the browser caches each shared module per referring page and no shared cache can hold them

public_html/.htaccess:28 wraps the lab no-store rule in <If "%{REQUEST_URI} =~ … || %{HTTP_REFERER} =~ …">, and Apache adds Vary: Referer,Accept-Encoding to every response that passes it, cadcraft.js included. A browser's cache entry for a shared file is then keyed by the referring page: the same assets/js/*.js fetched under /use/bimcraft after /use/cadcraft, or /library, misses; a CDN or shared cache cannot store the assets. With 400 requests per app start this counts. Change: keep <If "%{REQUEST_URI} =~ m#^/assets/apps/[^/]+/a Discord member/#"> for a Discord member builds and send Cache-Control: no-store for lab pages from PHP (header.inc when is_a Discord member) instead of the Referer match.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
PlatformPERF.66 Link to PERF.66Comment on PERF.66 Server: Apache and TLS settings for a 400-request app start: MaxKeepAliveRequests 100, no OCSP stapling, RSA-2048, no HSTS, MaxConnectionsPerChild 0

Measured: a TLS handshake is 25 to 30 ms on localhost (RSA-2048, session tickets off by certbot, no stapling: "OCSP response: no response sent"); with MaxKeepAliveRequests 100 and six connections a 400-request CadCraft start forces at least four extra handshakes (each about two round trips plus CPU for a visitor). Prefork MaxRequestWorkers 150; 36 to 40 children at 50 MB (81 MB max) = 2 GB; MaxConnectionsPerChild 0 so a mod_php child never recycles. HSTS is on api.getcadcraft.com and the forks, not on getcadcraft.com. Changes: MaxKeepAliveRequests 1000 (or 0) in /etc/apache2/apache2.conf; SSLUseStapling On plus SSLStaplingCache shmcb:${APACHE_RUN_DIR}/stapling(128000) in mods-available/ssl.conf; certbot --key-type ecdsa for getcadcraft.com and api (halves handshake signing); Header always set Strict-Transport-Security "max-age=31536000" on getcadcraft.com; MaxConnectionsPerChild 10000 in mpm_prefork.conf. All moot or smaller after PERF.1 (HTTP/2), which these numbers also strengthen.

– Site and services Decision Time 1 Owner: apache2.conf, mods-available/ssl.conf and mpm_prefork.conf are server-wide (about 80 vhosts); the ECDSA certificate and HSTS header are this site's. docs/0.9-sessions/perf-wave3/
PlatformPERF.67 Link to PERF.67Comment on PERF.67 Server: MySQL settings versus the data on the box: a full 1 GB buffer pool for 5.2 GB of data, table cache overflowing, slow log off

Measured: innodb_buffer_pool_size 1 GB with 1,024 free pages and wait_free 74,540 against 5.2 GB of data plus index across 56 schemas (cadcraft_db 1.23 GB; the two analytics tables alone are 1 GB); table_open_cache 4000 full with 110,482 overflows; Sort_merge_passes 1.1 M with a 256 KB sort buffer; innodb_io_capacity 200 on an SSD; slow_query_log OFF and long_query_time 10 (7 hits in 12 days; nothing below it is known). Also from the SQL track: 64,469 COMMITs at 2.76 ms (innodb_flush_log_at_trx_commit = 1, every autocommit an fsync) and SET time_zone run 1.28 M times = one per connection (no persistent connections). Changes: innodb_buffer_pool_size 4G (SET GLOBAL plus mysqld.cnf), table_open_cache 8000, sort_buffer_size 1M, innodb_io_capacity 1000 / max 4000, slow_query_log ON with long_query_time 0.5 and log_queries_not_using_indexes capped, init_connect = "SET time_zone='+00:00'" if persistent connections are wanted; flush_log_at_trx_commit 2 is a server-wide trade (a second of rows lost on a crash) the other sites would share: the analytics row removes most of the fsyncs anyway. Healthy: pool hit rate 99.9 %, 2 disk temp tables of 3.6 M, redo 430 MB a day, max_used_connections 44 of 151.

– Site and services Decision Time 1 Owner: shared MySQL (56 schemas); all settings are dynamic, no restart; the pool adds about 3 GB to mysqld's 2.8 GB RSS (15 GB free). docs/0.9-sessions/perf-wave3/
PlatformPERF.68 Link to PERF.68Comment on PERF.68 Server: disk at 75 % with 90 GB reclaimable (a 56 GB Rust build tree, tmp/ build trees, both 0.8 downloads, a lang-overlay cache); logs rotated monthly

Measured: / is 290 of 388 GB. desktop-shell src-tauri target 56 GB (the forks' target moved to W: on 2026-11-08; this one did not; memory rule: never rebuild forks on the VPS), stores/build 18 GB, tmp build trees about 13 GB (editions, public-repos, studio-suite-0.9-a Discord member, devshell, live-apps-www*), downloads 0.8 and 0.8.1 both on disk (2.1 GB each; 0.7 and older moved to the CDN), three byte-identical frozen copies per app (hardlinking would save about 1.3 GB), tmp/cache/lang-overlay 981 MB with no TTL; access.log 345 MB a day uncompressed for a month (2.76 GB now). Noise: mec-broker logs two lines a minute when idle, a Discord member-sandbox-sync logs the same schema drift (team_join_codes.used_at) every 10 minutes, mail-poll logs a PHP array_merge(stdClass) error every 5 minutes (a ContactMail bug). Changes: move or delete the Rust target, clean tmp/ after each release, TTL on lang-overlay, hardlink identical files across frozen versions in bin/release-app.php, publish 0.8 downloads to the CDN like 0.7, weekly (or daily access) logrotate, quiet the idle broker line, fix the sandbox drift and the mail-poll error. About 90 GB back (75 % to about 52 %).

– Site and servicesTests and tooling Planned 1 Owner: /etc/logrotate.d is server-wide; tmp/downloads-replaced (4.7 GB) is the "never deleted" policy and stays. docs/0.9-sessions/perf-wave3/
FixPERF.69 Link to PERF.69Comment on PERF.69 Website: the developer API makes three writes per keyed call (last_used_at, throttle, request log), health polls included

DevKeys::inspect runs UPDATE a Discord member_api_keys SET last_used_at, Throttle::hit an INSERT and DevApi::log an INSERT on every call, 200s on /v1/health and 404s too; each autocommit is an fsync with innodb_flush_log_at_trx_commit 1. Load test: api host /v1/health 679 req/s, /v1/endpoints 763, /v1/status 364 at c=10, zero failures (the floors are fine: health 3.6 ms). At 1000x (an integrator polling once a second) the writes are the cost. Change: update last_used_at at most once a minute per key, skip the request log for health 200s, keep it for everything else; a Discord member_api_requests is pruned at 30 days already.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
Sanity checkPERF.70 Link to PERF.70Comment on PERF.70 Server: operations checks: two cron jobs without a lock, status-check's serial probes, no Apache scoreboard, static files gzipped per request

bin/mail-poll.php (IMAP, every 5 min) and bin/a Discord member-sandbox-sync.php (every 10 min) have no flock while every other job has one; status-check runs 7 probes serially with 12 to 40 s timeouts plus 5 s retry sleeps (about 2 min worst case inside a 5-min slot; MeCentral timed out once on 2026-11-07). mod_status is on (Require local) but /server-status is rewritten to the 404 page by the catch-all .htaccess, so there is no scoreboard for bin/admin-perf.php --http or future measuring. Static JS is gzipped on every request: cadcraft.js 198 to 59.5 KB costs 9.0 ms p50 against 0.7 ms for an uncompressed file through the same .htaccess, 147 req/s at c=10 and p99 1,029 ms at c=20 (PERF.16 carries the fix: pre-compressed .br/.gz siblings and an immutable one-year cache). Changes: add the flock, run the probes with curl_multi or cap the total at 60 s, a <Location /server-status> with RewriteEngine Off in this site's vhost. Load floors for the record (c=10, keep-alive, zero failures): health 775 req/s, catalog 303, /status 189, / 80, /apps/cadcraft 58, /use/cadcraft 17.4 req/s at p50 586 ms (its 830 KB page is the throughput ceiling: PERF.2, PERF.3).

– Site and servicesTests and tooling Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.71 Link to PERF.71Comment on PERF.71 Code: eight app mains are one closure of 2,500 to 5,800 lines each; split by topic

The AST pass over 5,500 modules (204,968 functions) found each app's main a single init closure: bimcraft init 5,759 lines with 1,557 nested functions, markupcraft 4,755, takeoffcraft 4,509, modelcraft 3,858, partcraft 3,770, cadcraft 2,898, civilcraft 2,848, detailcraft start 2,478. Everything inside shares one scope, so nothing can be dynamically imported (the start-up row), tested alone or reasoned about; this is the single biggest piece of sloppy structure the owner asked about. The report (docs/0.9-sessions/perf-wave3/code-quality.md Q1) outlines each by its inner functions and proposes modules per topic (BimCraft: IFC open/save, fd, QTO, find, clash, props, nine context modules; the same per app). L per app, M for CadCraft; one app per lab pass, behind the existing browser tests (tools/<app>-test, bin/test-<app>*.mjs).

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App code Planned 20 – docs/0.9-sessions/perf-wave3/
FixPERF.72 Link to PERF.72Comment on PERF.72 Code: 4,300 one-line functions that only pass arguments on (forwarder adapters rebuilt per sub-UI, help: () => t() closures)

tools/perf/quality-forwarders.mjs: 2,299 forwarders plus 2,007 thin wrappers, 278 getters, 401 constant-return functions, 2,683 one-use helpers. Worst: civilcraft.js 182 forwarders (units: () => units(), say: (m) => say(m), row: (...x) => row(...x) rebuilt for four sub-UIs, lines 157-165), bimcraft.js 143, partcraft.js 117, landscape-ui.js 52 (the same {edit, LS, U} trio six times), commands.js 147 help: () => t('…') closures plus seven ask wrappers. This is read and parse cost and noise, not runtime (inlined). Change: pass the declarations by name, one adapter object per app shared by its sub-UIs, help: 'key' resolved at display time. S per file.

CadCraftBimCraftCivilCraftDetailCraftPartCraft App code Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.73 Link to PERF.73Comment on PERF.73 Code: copies that drifted and hand copies with no sync tool (six inflate implementations, xlsx.js in four variants, input-mouse.js in eight apps)

The byte-copy system works where a tool owns the copy (1,011 of 1,043 copy groups). Outside it: inflateRaw plus deflateZlib byte-identical in lib/3dm and lib/chip, six inflate implementations in lib and crc32 in 316 files (one lib/zip); xlsx.js six times in four hand-edited variants (one lib/xlsx); pdf-snap.js and takeoffcraft snap.js diverged; lib/walls ahead of bimcraft wall-square.js (two solvers for one wall); 31 drifted file pairs in all; whole-file hand copies with no tool: input-mouse.js in eight apps, wheel-pad.js in five, panes.js in three, params.js, framing.js, bcf.js, render-tools.js, carbon-factors.js, keynotes.js; CadCraft's m3d copy stale because its tool refuses to run (FX.143). Change: one owner per copy (lib/ or the shell) with a sync tool, and bin/test-sync-copies.mjs that runs every sync tool with --check (130 tools; some write without the flag: give them one first). Also the STEP string helpers S/E/L/B typed into five IFC writers (civilcraft three, detailcraft, partcraft) belong in lib/ifc/step.js.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App codeTests and tooling Planned 4 – docs/0.9-sessions/perf-wave3/
FixPERF.74 Link to PERF.74Comment on PERF.74 Code: shared utilities written hundreds of times: vector math 2,088 implementations, csv cell 431, point-in-polygon 243, esc 198, uid 188, download 163

tools/perf/quality-utils.mjs counted 6,943 implementations of 24 utility names (21,260 lines): vector math 2,088 (516 distinct bodies in 1,094 files), area 299, point-in-polygon 243 (identical clusters of 17, 12, 9, 8 and 6), bbox 190, lerp 69, clamp 68; text and bytes: esc 198, uid 188, download/saveBlob 163, csv cell 431, base64 59. The shell exports none of these; lib/units is the one shared table that is used everywhere. Change: lib/geom2d (vec, bbox, area, point-in-polygon, lerp, clamp) and a shell util.js (esc, uid, download, csv, base64) synced into the builds like host.js; mechanical replacement guided by the cluster list, with the quality scripts kept as the check so the count only goes down. Also shell-mount.js (a 48-line block identical in seven apps) and ui-util.js (eight variants of one 45 to 76 line file) become synced shell files, and the Studio Live stateCore plus diff and invert hand-copied in three apps and joinFromPage in six become a shell live-state.js.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App codeApp shell Planned 6 – docs/0.9-sessions/perf-wave3/
FixPERF.75 Link to PERF.75Comment on PERF.75 Code: deep clones by JSON round trip (205 sites, some inside merge loops) and 24 timers polling faster than once a second

Perf-relevant sloppiness confirmed by reading the top sites. Clones: 205 JSON.parse(JSON.stringify()) plus 112 clone helpers; takeoffcraft/a Discord member/learn.js clones inside merge loops (11 sites), clipboard.js and dwg-keep.js clone document-sized objects: structuredClone or a cloneDoc that copies only what changes. Timers: cadcraft live.js:763 and markupcraft live-ui.js:71 setInterval(push, 33) build presence and JSON.stringify it 30 times a second while idle; hazard-ui and people-ui animate with setInterval 33; countall-ui waits on a worker with a 20 ms poll; plc-ui 50 ms; 250 to 500 ms pollers elsewhere. Change: event-driven pushes with a dirty flag, requestAnimationFrame for animation, promise-based worker replies; a check that lists any setInterval under 1 s. Also: a console.log in bimcraft fem/frame-push.js:155 inside the solver loop, 12 new RegExp inside loops (takeoffcraft xlsx-read.js:74 on file open).

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App code Planned 2 – docs/0.9-sessions/perf-wave3/
FixPERF.76 Link to PERF.76Comment on PERF.76 Code: O(n squared) searches in kernel code (ModelCraft mesh ops, the shared m3d model, CivilCraft LandXML) and render functions that rebuild whole panels

Nested-loop searches with includes/some inside N by N loops: modelcraft mesh/ops.js:421 subdivide (also :471, :820, :915), m3d model.js:693, :778, :1637, :1957 (in both ModelCraft and CadCraft's copy), civilcraft landxml.js:298-322. These are the places a 10x model turns into a 100x wait; the 3D track's numbers (ModelCraft push/pull and undo at 500k faces) land here. Change: Sets and Maps keyed by vertex or id, measured before and after with tools/perf/scale-bench.mjs and the apps3d scripts. Render functions that rebuild and re-query the DOM on every call (196 querySelectorAll and 315 addEventListener inside render functions; takeoffcraft.js sheet list on every sheet switch, shell app-workspace.js, supercadcraft.js, cadcraft renderLayers and renderSelTotals): keyed children and incremental updates, measured on the 2D track's 2,000-sheet set.

CadCraftModelCraftTakeoffCraftCivilCraft App codeOur engines Planned 4 – docs/0.9-sessions/perf-wave3/
FixPERF.77 Link to PERF.77Comment on PERF.77 Code: PHP service layer: 1,457 prepare() with no shared helper, LibraryResult unused, 35 classes with their own ok()/fail(), components re-running one query

tools/perf/quality-php.php: the $st = db()->prepare(…); $st->execute([…]); $st->fetch(…) idiom appears 666 times across 204 classes, six classes define a private row() helper of their own, 626 hand-written ['ok' => …] results while LibraryResult exists and is used by zero classes. Pages where the controller and its components call the same service method with the same arguments more than once: /admin/library LibraryReports::countOpen() three times, /admin/messages Contact::countsByStatus() three times, /supercad SuperCadCheckout::seatsLeft() four times, the navbar Downloads::current() twice on every page. Change: db_row/db_rows/db_val/db_exec in core/functions/helpers.php and result_ok/result_fail (or adopt LibraryResult), added once (S) and adopted class by class as they are touched (M); pass page values down to components instead of re-querying. The SQL track's memo row covers the lookup side.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.78 Link to PERF.78Comment on PERF.78 Code: dead modules and exports, god functions and cyclomatic hotspots beyond the mains

Confirmed dead: cadcraft addons/windows/lib/cutlist/cutlist.js (190 lines, a stale copy nothing imports), addons/trusscraft/lib/layout.js in four apps (tests only), modelcraft anim/skin-gpu.js, compute/parity.js twice, partcraft eng-data and eng-units stubs; 32 dead exports (about 54 KB: partcraft worker solvers, the openfoam runner) plus 26 orphan modules (about 141 KB). Name-level dead code across the 5,020 namespace and dynamic imports needs a bundler dry run (one tree-shake pass per app as a report, nothing shipped bundled). Hotspots: 120 functions over 300 lines and 618 with cyclomatic complexity 40 or more: readFamily 234, writeDwg 769 lines at 214 (plus convert 195, buildModelParts 182), vla-host get 207, validateItem 194, buildSite3D 190, dxf entityFrom0 187 and writeDxf 159, pdf-write writeMarkedUp 182, writeCivilIfc 702 lines, model-ui mountModel 1,804 lines, res-ui 1,407, shell app-panels 1,742 and app-settings 1,521. Change: table-driven dispatch for the entity switches, one module per panel section, delete the dead files; each behind the existing tests.

CadCraftBimCraftModelCraftCivilCraftDetailCraftPartCraft App code Planned 4 – docs/0.9-sessions/perf-wave3/
Interface roadmap — wave 1 26
PlatformUI.1 Link to UI.1Comment on UI.1 Interface pattern: one slot system every layout maps onto, written down, with a reference mockup

Owner 2026-11-09: our layout efforts do not feel "strong and stable and consistent"; main menu bars, ribbons, panels and footer bars from every mainstream interface must fit one patterned system cleanly. What exists: the layout engine (assets/js/app-workspace.js, 1,050 lines) with 15 block kinds over 12 fixed places (app-workspace-format.js PLACES: top, bottom, left-panel, right-panel, stage-top/bottom/left/right, view-top/bottom, float, none) and six built-in layouts (docked, toolbox, autorev, duncan, vector, sketch), each a data file. The pattern is real but was never drawn or written as a rule book, so each new layout (Duncan, Vector, Sketch) added its own block and its own CSS section. This row: (1) the mockup made today, pattern.html: the slot map, the seven layouts (the six plus the Drafting layout of UI.12) drawn on the same slots, the block families (menu bars, ribbons and tool rows, palettes and panels, bars, overlays, windows) and the consistency rules; (2) a spec docs/INTERFACE-PATTERN.md from it: the places (adding view-corner overlays, UI.10, and windows, UI.2/UI.3), what each family may do in each place, the one tear-off, one "…" menu, one pressed/disabled/hover state, one dialog baseline, one icon set, one keyboard contract, one storage key family; (3) the conformance test (bin/test-app-workspaces.mjs, 12 contract points) gains a point per rule, so every layout in every app is held to the same pattern from then on. Every later row in this wave builds inside this spec; a new block that does not fit it is refused. Owner addition the same day (see UI.21): the pattern is zones and dockable units, a ribbon being tool units docked along the top of the canvas and a palette the same unit docked at a side, the way panels dock, group and collapse in image editors; the mockup and spec state it that way.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftSuperCad Studio App shell Partly built 2 Owner look at the mockup (docs/0.9-sessions/interface-wave1/pattern.html); the spec is written after that look. docs/0.9-sessions/interface-wave1/SUMMARY.md
PlatformUI.2 Link to UI.2Comment on UI.2 One tear-off: every bar, menu, palette and panel moves to its own window the same way

Today there are four ways to put a piece of the interface in a window: the ribbon has its own tearOff()/dockRibbon() in app-workspace.js (window.open, stylesheet copy, key forwarding, about 60 lines), the toolbox, tool rows and status bar use app-workspace-popout.js popout(), the app panels use app-panels.js (its own ONE "panels window", adoptNode, Window Management API), and the classic toolbar and SuperCad walls use app-toolbar-tear.js and api.tearable(). Each copies stylesheets and theme differently, forwards keys differently and remembers (or does not remember) its window differently; closing behaviour differs too. Change: one window service in the shell (app-windows.js, grown from popout.js) that every piece uses: open from a click, move the nodes (handlers and state go along), mirror <html>/<body> attributes, stylesheets, interface size and icon style, forward keys that are not typing, give the window a title and the app's icon, dock back on close, on "Put it back" or by dragging the grip back over a drop target (UI.5). The ribbon, panels, toolbar and SuperCad code become callers of it. One "…" menu (shell-panel-menu) for all of them with the same entries in the same order: Move to a new window / Put it back / Float / Dock left / right / bottom / Hide. Test: the conformance test tears off every block that allows it and brings it back, in every layout.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftSuperCad Studio App shell Planned 4 – docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.3 Link to UI.3Comment on UI.3 Multi-monitor: any number of windows, remembered per app and layout, restored in one click

Owner: "one of our biggest desires is to be multi monitor capable". Today the panels window is one window for both panels (app-panels.js), each torn-off block is one more pop-up, and only the panels window remembers its position and screen (cc-app-settings:<slug> panels.wins, Window Management API "Put it on my other screen" in Chromium); a block's pop-up opens at a default size at the pointer's screen and is forgotten on reload; a browser cannot reopen windows without a click, so after a reload the shell can only offer "Open it". Change, on the one window service (UI.2): each window keeps {screen, x, y, w, h, contents} under cc-windows:<slug>:<layout>, the left and right panels may be two windows, a palette may leave its panel for a window of its own, several bars may share one window (a tool strip window), and on start the status line offers "Restore my 3 windows" (one click opens them all, the browser allows that inside one click), with getScreenDetails() where the browser has it and the last position elsewhere. The layout file (format v2, UI.6) may say a block starts in a window ("place": "window"), so a layout made for two screens is shareable. Studio does the same through real windows (UI.4). Works on the page too: a floating palette or bar (place float) can be dragged anywhere over the drawing and is remembered the same way, so "reorganized on the page/canvas" is the same feature with the window left out.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftSuperCad Studio App shellDesktop host Planned 5 – docs/0.9-sessions/interface-wave1/SUMMARY.md
PlatformUI.4 Link to UI.4Comment on UI.4 Studio: torn-off windows are real windows, placed on the right monitor and remembered, on Windows, macOS and Linux

In CadCraft Studio a window.open becomes a real Tauri window through on_new_window in desktop-shell src-tauri/src/lib.rs (built on about:blank; the engine loads the asked page into the window it is handed). This path has crashed Studio on Windows before (TakeoffCraft pop-up, 0.6 report) and the 0.8.1 hand-off was never tested on a Windows build. This row: the window service (UI.2/UI.3) in Studio = one Tauri window per torn-off piece with the app's icon and a title, placed on the monitor the person dragged to (Tauri monitor list, no Window Management permission prompt), its place remembered with the window ids that survive a restart (MCP-WINDOW-IDS), restored at start without a click (the desktop may do what the browser may not), closed with the main window, and a startup log line per window (PERF.18's log). Test on ws11a (two monitors) and the owner's Mac; tablets stay without pop-ups (the shell's mobile rule).

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftSuperCad Studio Desktop host Planned 3 A Windows build and a hand test on two monitors (the pop-up hand-off is unverified since 0.8.1; see memory studio-popup-handoff-crash). docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.5 Link to UI.5Comment on UI.5 Arrange by dragging: move any block, palette or bar to another place and save it as your own layout

Owner: each layout can be customized, or a person creates their own. Today "Arrange your own layout" (WS.25, app-workspace-arrange.js) is a form: a dialog with radio buttons for where the toolbox goes, which panel holds which palette, which bars show; one palette stack, no split editors; a new layout is made from the form, not from the screen. Change: an Arrange mode (Settings › Layout › Arrange, or the grip's "…" › Arrange) that draws the slot map of UI.1 over the live page as drop targets and lets the person drag every block (ribbon, tool rows, menu bar, toolbox, palettes, info box, quick bar, view tabs, status bar, command line) by its grip to another allowed place, to float, or to a window (UI.3); blocks that cannot go there say so on hover (the format's places list is the rule); a palette may be dragged out of its stack into the other panel or into a stack of its own (two palette stacks: a format v2 change, UI.6). Done = the result saved as "My <layout name>" (file:my-<id>, the existing WS.17 path) and chosen; Reset puts the built-in back. Every drag is also an undoable step while the mode is on. Replaces the form, keeps its validation (validateLayout, resolveLayout). Test: a drag of each block kind in CadCraft and BimCraft, saved, reloaded, exported and imported again byte-identical. Owner addition the same day (UI.21): the drop targets are the dock zones of the one dock manager, so a ribbon group dragged to a side column becomes a palette there, a palette dragged to the top joins the ribbon, and either dropped on another unit joins it as a tab.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shell Planned 6 – docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.6 Link to UI.6Comment on UI.6 Customize what a layout holds: the tools in each ribbon tab, tool row, palette and menu; layout format v2

A layout today says WHERE the app's blocks go; what a ribbon tab or tool row holds comes from the app's adapter (its ribbon(), toolbox() and the input registry) and cannot be changed, except My tools (8 slots, WS.21) and the menu style's toolbar arrangement. Change: layout format v2 (app-workspace-format.js VERSION 2; v1 files still read): an optional "tools" section per block with add / remove / move / rename of tools and groups by registry id ("ribbon": {"tabs": [{"id": "home", "groups": [{"id": "draw", "tools": ["line", "polyline", "circle", "+cmd:layer.new"]}]}]}), a "menubar.menus" override, "palettes.stacks" (two stacks, UI.5), "place": "window" (UI.3), and "look.tone" (UI.16); every id checked against the app at resolve time, unknown ones dropped with a note as today. The UI: right-click a tool › Remove from here / Add a tool… (the My tools picker, grown to any group) / Move left / right / New group, in every block, saved into the person's derived layout (UI.5). Reset per block. The add-ons repo carries a byte-identical copy of the format (examples/layout-drawing-first/tools/workspace-format.js; the test checks it) and docs/LAYOUTS.md gains the v2 section. The 20-tool "visible" count of tool rows and the ribbon's large/small split become per-group choices here too. Owner addition the same day: where a ribbon group's name sits is a layout setting, not a constant: look.names "below" (under the tools, the ribbon default) or "above" (over them, the palette and tool-row default), overridable per zone in the layout file and switchable in Settings › Layout; the dock manager (UI.21) and the ribbon (UI.11) read it, and the mockup's demo has the switch. Also (owner): a bar never wraps; look.overflow = "more" (default: items that do not fit fold from the right into ⋯, Add-ons stays last), "compact" (one ☰ button) or "drop" (the bar takes its own row in the top zone until there is room); for the menu bar, tool rows and the status bar.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shell Planned 6 – docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.7 Link to UI.7Comment on UI.7 A layout per app, finished: each app remembers its own, a switcher in the title bar, SuperCad Studio lists every layout

Owner: each app can use a different interface. FX.82 (0.9, partial) added a checkbox in Settings › Layout that keeps the pick for that app alone; the rest is open. Change: (1) the per-app pick is the default (cc-workspace:<slug> first, the global cc-workspace as the fallback), shown in Settings as "This app" / "Every app" with the app's name; (2) a workspace switcher in the title bar and the Vector/Drafting menu bars (the "2D Drafting ▾" drop-down of the owner's reference screenshot): the list of layouts with the person's own first, switching in place without a reload (the engine's decide() already does this; Work environments WS.24 still reload: they stop reloading when only the layout changes); (3) SuperCad Studio's manifest lists all seven layouts, not only docked, and its hosted apps follow the host's pick or keep their own (the guest rule stays: a guest never lays out the host's page); (4) ?workspace=<id> stays for one page. Help common/layout-ribbon, layout-toolbox and layout-own updated; words shell.ws.switch*.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftSuperCad Studio App shell Planned 3 – docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.8 Link to UI.8Comment on UI.8 Title bar: quick-access tools you choose, the workspace switcher, the document name and a command search box

The owner's reference (testfiles/dropai-BkUApXoy/paste-2026-11-09T00-47-03.png, a drafting program in the browser) has a second strip under the menu bar: app mark, quick-access tools (new, open, save, save as, print, undo ▾, redo ▾), the workspace drop-down, the document name centred, and "Type a keyword or phrase" search at the right. Our titlebar block has quick (the well-known actions only: open save undo redo select clear fit), the document name, Help and Settings. Change: quick takes any registry command id (format v2, UI.6) and a person adds or removes tools there by right-click (the My tools picker); undo and redo get a ▾ with the last steps when the app's history offers them (CadCraft has one); the switcher (UI.7); a search box that is the command finder (Ctrl+K, ccPalette) inline, with help topics and Library items in its results as the finder already lists them; the name shows the dirty mark and a tooltip with the path. One titlebar for the menu-bar layouts too: the Vector menu bar's brand slot and the title bar merge into one block with a "menus" option, so a layout never has two brand marks (owner rule 2026-11-05: the brand always visible, once). Owner addition the same day: the title bar is the ABSOLUTE TOP of every app and layout: the app mark and name at the far left with the area chip (Browser / Lab / Studio and the version), which open the Craft menu (UI.24); a dock zone beside them where the menu bar sits by default (it can also dock underneath in the top zone, or tear off like any unit, UI.21); this row's quick-access tools and workspace switcher; and Find, the search box, at the far right. Nothing else is in that row: the module buttons of the owner's screenshot (Find, Help, Save for offline, Problems, Calc sheets, Design reasons, Lessons, Field packs, Engines, Drawing details, Settings, Full screen, Paper / Model) move into the Craft menu, the Add-ons list or the title bar's own blocks as UI.24 says. The mockup's demo draws it.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shell Planned 2 – docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.9 Link to UI.9Comment on UI.9 Command line anywhere: docked in the status strip, floating over the drawing, in its own window, or hidden

Duncan docks a command line in the status strip (statusbar.command, 0.9 DU.1), and it only hands its text to the command finder; the reference screenshot floats one over the drawing with history above it and a ▾ to open the log. CadCraft has a real command line of its own (cad-cmdline-09.md) in the classic layout only. Change: a commandline block of its own (places: stage-bottom, bottom, float, window; options history, log, width), filled by the app when it has one (CadCraft's, with its LISP and keyword prompts) and by the finder elsewhere; dragged by its grip to float anywhere over the drawing or to a window (UI.2/UI.3), remembered; Ctrl+9 shows/hides it as people expect; typed input goes to it from anywhere in the drawing when the layout says so (the classic rule). Works in every layout the person adds it to (UI.5), default in Drafting (UI.12) and Duncan.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shellApp code Planned 3 – docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.10 Link to UI.10Comment on UI.10 View overlays: view cube and compass, navigation bar, viewport label, axes icon, in the corners of the drawing

The drafting reference shows four things over the drawing itself: a view cube with a compass (N/E/S/W, "TOP", a WCS drop-down) top right, a navigation bar (a Discord member, zoom, zoom window, orbit) at the right edge, a viewport label "[-][Top][2D Wireframe]" top left, and an axes (UCS) icon bottom left. The engine has no corner places: view-top and view-bottom are full-width strips, and each app draws its own view cube (BimCraft's display modes beside the view cube, GH.3-B) or none. Change: four view-corner places (view-tl, view-tr, view-bl, view-br) in the format, each a small stack of overlays that never take pointer events outside their boxes; shared overlay blocks: viewcube (the app's 3D view orientation through the adapter's views API, with the compass and the named views), navbar (a Discord member, zoom extents, zoom window, orbit, walk from the registry), viewlabel (view name, display mode, scale, the [-] menu with the app's view commands), axes (the app's UCS/axes drawing, or the shared one). Each is a block a layout turns on, so Drafting (UI.12) and Sketch have them and Toolbox does not. Apps keep their own where they have more (BimCraft's display modes feed viewlabel). Test: each overlay present and placed in every app that has 3D, with the drawing's pointer events untouched between them.

CadCraftBimCraftModelCraftCivilCraftDetailCraftPartCraft App shellApp code Planned 4 – docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.11 Link to UI.11Comment on UI.11 Ribbon and footer: group flyouts, inline controls in a group, contextual tabs that light up; a status bar with toggles and a customize menu

Ribbons: our ribbon block draws each group's large tools and stacked small tools with the group's name under it; the reference adds a ▾ on the group title that opens the rest of the group (tool rows have this: details.tr-more), inline controls inside a group (the layer drop-down, colour / linetype / lineweight pickers "ByLayer" in the Properties group, a measure field), and a contextual tab that appears lit in the app's colour when something is selected (ours exists, A.contextTab, but looks like the others). Change: ribbon groups take "more" (the rest of the group under ▾) and "slots" (the adapter's slot controls moved in, as the info box does), large / small per tool from format v2 (UI.6), a contextual tab coloured by the app's layer and a one-line label of what is selected, keyboard access by Alt. Footer bars: the status bar today holds the app's status line, the selection count and the layout's name; the reference's holds the Model / Layout tabs, mode toggles (grid, snap, ortho, polar, object snap, tracking, lineweight, transparency, selection cycling, annotation scale 1:1, workspace, isolate) and a gear that chooses which toggles show. Change: statusbar takes "toggles" (registry commands with pressed state, drawn as icon buttons lit when on, the app says which it has through pressed()), a scale slot, and a customize menu (the gear) that ticks toggles on and off, saved in the person's layout. Model / Layout tabs are viewtabs mode "all" at view-bottom, with a "+" when the app can add a view (CadCraft paper space, BimCraft sheets). Owner addition the same day: a ribbon group has no collapse control; collapsing belongs to palettes and floating panels (UI.26), and a unit dropped into a ribbon zone opens. The ribbon as a whole still minimizes to its tab strip (DF.27), from the tab strip, never per group.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shell Planned 4 – docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.12 Link to UI.12Comment on UI.12 Drafting layout: menu bar, quick-access strip, ribbon, file tabs, view cube and navigation bar, command line, Model / Layout tabs, status toggles

The owner's reference screenshot is a 2D drafting workspace of the kind people who come from the long-established drafting program know: a menu bar (File Edit View Insert Format Tools Draw Dimension Modify Window Help), the quick-access strip with the workspace drop-down and search, a ribbon (Home Insert Annotate Parametric View Manage Output Add-ins Collaborate) with Draw / Modify / Annotation / Layers / Block / Properties / Groups / Utilities / Clipboard / View groups, file tabs (Start, Drawing1*, +), the drawing with a viewport label, a view cube with compass, a navigation bar and an axes icon, a floating command line, Model / Layout1 / Layout2 tabs and a status bar of toggles with the scale and a customize gear, in a dark chrome. None of our six built-ins is this one (docked and autorev are BIM ribbons, Vector and Sketch are palette layouts). This row: a seventh built-in, "Drafting" (assets/workspaces/drafting.ccworkspace.json; the name is ours, never the program's: the vendor-names rule), made only of the engine's blocks (menubar set "drafting", titlebar with quick + switcher + search, ribbon with more/slots/contextual, viewtabs open at view-top, the four view overlays, commandline float, viewtabs all at view-bottom, statusbar toggles + scale + customize, look.tone dark), listed in all eight apps and SuperCad Studio; CadCraft's adapter fills every slot, the other apps fill what they have (resolveLayout drops the rest with a note, as today). Every pixel from our tokens and icons: the layout copies where things are, never another program's artwork. Conformance test, help topic common/layout-drafting, es-MX and id words, a screenshot for /download and the release note.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shellApp code Planned 4 UI.8 to UI.11 (the blocks it is made of); the owner's look at the mockup (UI.1). docs/0.9-sessions/interface-wave1/SUMMARY.md
Sanity checkUI.13 Link to UI.13Comment on UI.13 Icon quality bar: a review sheet of every icon at 16, 20, 24 and 32 px in every style and both themes; redraw what fails

Owner 2026-11-09: "Our icons look like crap. We want cleaner colorful and theme colored icons." The CadCraft set (assets/js/app-workspace-icons.js, 0.9 DU.1, 2026-11-05) has 114 drawings on a 32-unit grid with a 1.5-unit round stroke and flat two-tone fills (roles w, a to e, s1/s2, t); 37 wall-tool icons were added by WT.1. It was drawn in one day, reviewed on one sheet at one size, and used only where a layout opts in (toolrows.icons "set", the Vector palettes); everywhere else the apps show Bootstrap Icons (one weight, grey) or their own drawings, so the screen mixes three icon families. This row, before more drawing: (1) a review sheet page (tools/icons/sheet.mjs → a static HTML in the lab) rendering every icon at 16 / 20 / 24 / 32 px, full colour, outline colorized and theme colour (UI.15), light and dark, next to the Bootstrap icon and the app drawing it replaces; (2) drawing rules written in the module header from the owner's look: grid, stroke and corner radius, the five fills and when each is used, what a 16 px icon may still show (no text, no detail under 2 units, one idea), silhouette first; (3) a lint (tools/icons/lint.mjs) that fails an icon off the grid, with a stroke under 1 or a sub-2-unit detail; (4) redraw every icon the sheet shows as muddy at 16 px (a first look at the set suggests the dimension, text and view icons). The sheet stays as the standing check (the test runs the lint).

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shell Planned 3 Owner look at the sheet; his word on the drawing rules (stroke, corner radius, fill palette) before the volume row UI.14. docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.14 Link to UI.14Comment on UI.14 One icon family everywhere: the CadCraft set in every layout's ribbon, toolbar, toolbox, menu and palette, and the missing tools drawn

Today the set shows only in Duncan's tool rows and the Vector/Sketch palettes; the ribbon, the classic toolbars, the menu styles, the toolbox, the File menu, the pet palette and the status bar still show Bootstrap Icons or the app's own drawings, and about 270 specialist tools (the 2026-11-05 count: BimCraft MEP and families, CivilCraft alignments and water, DetailCraft connections, PartCraft features, MarkupCraft and TakeoffCraft marks) have no drawing in the set at all. Change: (1) the engine's tool() and the input registry's icon lookup ask the set first for EVERY block and the classic toolbars (iconKey → iconNode; Bootstrap only as the last fallback, logged in the lab so the missing list is real); (2) draw the missing tools in sets per app, each set reviewed on the UI.13 sheet, one agent per app with the rules, about 30 icons a day; (3) the eight apps' own toolbar drawings (CadCraft's 1.6 px line icons, BimCraft's, …) retire where the set covers them, kept under Settings › Icons "Each app's own drawings" for people who prefer them; (4) add-ons name icons from the set by key (api.icons) so an add-on's tools match. Measured at the end: zero Bootstrap fallbacks in the eight apps' default toolbars and ribbons in the lab; the number of set icons in the release note. Owner addition the same day (UI.22): the drawings themselves, the inventory and the icon repo are UI.22, the library; this row is wiring that library into every block, layout and app so one family shows everywhere.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shellApp code Planned 8 UI.13 (the rules) first. docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.15 Link to UI.15Comment on UI.15 Theme-coloured icons, and the colorize colour carried to labels and eyebrows

Settings › Global › Icons offers Full colour (fixed pastel fills: --ic-a #8fd3f5, --ic-b #ffb870, …, the same on light and dark), Outline colorized (one colour the person picks, --cc-icon-color) and Each app's own drawings. Owner: "cleaner colorful and theme colored icons (see color, colorize setting of icons - labels/eyebrows should follow)". Change: (1) a Theme colour style: the fills and accent lines derived from the theme tokens at render time (the app's layer colour --layer-<app> for the accent s1, its complement for s2, fills as tints of the sheet toward the layer colour with the ink as the line), so icons follow light / dark and change with the app, and a "Colour" pick (app colour, a layer colour, or any colour) under it; (2) the Full colour fills get a dark-theme set (today the same pastels sit on #1D232B and look washed); (3) "Labels and eyebrows follow": a switch that gives group titles (the ribbon's group names, the tool rows' group labels, palette headers, tab labels, the status bar's section names) the icon colour too, finishing FX.91 (group names in the app's colour, partial) with one rule for every block; disabled stays plainly grey (FX.90) in every style; (4) a contrast check in Settings: the picked colour is shown against both sheets and a warning says when it fails 3:1 on either. CSS stays token-only (--ic-* in app-workspace.css); the icon module reads the theme once and on the cc-space change.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shell Planned 3 – docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.16 Link to UI.16Comment on UI.16 Layout chrome tone: a layout may be dark, light or follow the theme, with its own chrome tokens

The reference drafting interface is dark chrome around the drawing whatever the OS theme; ours takes --ws-chrome, --ws-line, --ws-blue from the site theme (cc-space light / dark), so a layout cannot ask for a dark frame on a light theme or a paper-white frame on dark. Change: look.tone in the layout format (format v2, UI.6): "theme" (default, as today), "dark", "light"; the engine sets data-ws-tone on the layout's regions and app-workspace.css defines the chrome tokens per tone (sheet, chrome, line, ink, muted, the blue, the tab colours) once, so every block in every layout reads the same six tokens and no block carries a literal colour (a lint over app-workspace.css for hex values outside the token block). The drawing surface keeps the app's own background setting. Settings › Layout shows the tone under the layout name and lets the person override it per layout. Icons follow the tone through UI.15's theme colours.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shell Planned 2 – docs/0.9-sessions/interface-wave1/SUMMARY.md
Sanity checkUI.17 Link to UI.17Comment on UI.17 Stability pass: every app × every layout × both themes × three widths on one contact sheet, the engine gaps closed, tests that hold

Owner: "I personally dont feel like our efforts are strong and stable and consistent." What exists: bin/test-app-workspaces.mjs (12 contract points per app and layout, green on 2026-10-24 for the two layouts of that day); the newer layouts were checked in CadCraft and BimCraft only (DU.1, VEC.1, SK.1 all "partial"); the engine inbox (docs/0.7-sessions/workspace-layouts.md § Inbox) lists gaps the per-app sessions found, most closed in the follow-up run, some still worked around in app CSS (z-index lifts, canvas sizing); FX.37 (tablets in portrait), FX.43 (packaged editions print no mount for toolbar tear) and FX.82 to FX.85 are partial. This row: (1) a contact sheet tool (tools/workspace-test/contact-sheet.mjs) that opens every app (eight plus SuperCad) in every layout, light and dark, at 1920, 1280 and 760 px wide with a showcase file, and writes one page of screenshots for the owner, re-run before every release and kept in the release notes results section; (2) run the conformance test for every app × layout and fix what fails, including the new points from UI.1; (3) remove the per-app CSS workarounds the engine now covers; (4) the partial rows above closed or re-filed; (5) a flake budget: the suite runs three times green on the VPS before this row is Built.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftSuperCad Studio App shellTests and tooling Planned 3 – docs/0.9-sessions/interface-wave1/SUMMARY.md
Sanity checkUI.18 Link to UI.18Comment on UI.18 Words, help, add-ons guide, release note and the "Pick your workspace" video for the wave

Every new word under shell.ws.* in en, es-MX and id with bin/test-shell-lang.mjs knowing the families (the language sweep carries the other 80 locales); help topics common/layout-drafting (new), layout-own (dragging, customizing, windows), layout-ribbon and layout-toolbox (the switcher, the status bar), arrange-panels (one tear-off); the add-ons repo docs/LAYOUTS.md v2 section and a second example layout that uses windows and corner overlays; the release note's interface section with the contact sheet (UI.17) and the icon count (UI.14); the 0.7 "Pick your workspace" video re-filmed on the Drafting layout with a ribbon torn to a second monitor (video-designer; the file handed to the owner, never posted).

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft Help and words Planned 2 The wave's feature rows built. docs/0.9-sessions/interface-wave1/SUMMARY.md
DecisionUI.19 Link to UI.19Comment on UI.19 Owner: which mainstream interfaces to mock next, per app, for interface wave 2

Built-in layouts after this wave: Ribbon and project browser (docked), Toolbox and info box, AutoRev, Duncan, Vector, Sketch, Drafting (UI.12). Owner 2026-11-09: each app has several mainstream interfaces people like; mock them so a person picks the one they like per app. Candidates not yet mocked, by app, for your pick (names here are for you; the layout names stay ours, see the vendor-names rule): CadCraft: the classic toolbars-and-command-line drafting screen of the 2000s (menu bar, docked toolbars on four sides, command window, no ribbon). BimCraft: the Toolbox layout already follows the Archicad pattern and AutoRev the Revit one; a Vectorworks Architect pattern is Vector. ModelCraft: Sketch is there; a Blender-style editor-areas layout (split editors, header per area, N-panel) and a Rhino-style layout (four viewports, command line, toolbar tabs, right panels). MarkupCraft and TakeoffCraft: the Bluebeam pattern (left thumbnails, tool chest and markups list in right tabs, a properties toolbar under the ribbon) and the PlanSwift pattern (left pages tree, right takeoff items list, a quantities bar). CivilCraft: the Civil 3D pattern is a ribbon with a Toolspace palette (Prospector / Settings / Survey tabs) at the left. DetailCraft: the Tekla pattern (ribbon, a side pane with Applications & components, a mini toolbar at the selection). PartCraft: the Fusion / SolidWorks pattern (toolbar tabs, a browser tree at the left, a timeline at the bottom, a marking menu at the pointer, a Command Manager). Pick the three or four that matter most for 0.10 and this row becomes wave 2's list; each is one layout file plus the blocks it needs (split editors and a timeline are the two blocks none of the above have).

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftSuperCad Studio App shell Decision Time – Owner's pick. docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.20 Link to UI.20Comment on UI.20 Theme switch: sun and moon icons for light and dark wherever the interface says "Paper" and "Model"

Owner 2026-11-09: where we say "paper/model" we switch to sun/moon icons for the light and dark themes. Today the theme toggle is a two-button form with the words Paper and Model: the site navbar (navbar.inc, titles "Paper space: light theme" / "Model space: dark theme"), the app bar in every app and its More menu on phones (app-bar.inc, words shell.bar.paper / shell.bar.model and their titles), the Settings radios (app-settings.inc already draws bi-sun next to "Paper"; shell.settings.theme.paper / .model), and the pre-paint script and site.js that read them. Change: one shared toggle component (space-toggle.inc) drawn as two icon buttons, a sun for light and a moon for dark, from the CadCraft icon set (UI.14: two new drawings, sun and moon, in all three styles) with the pressed state lit, aria-labels and tooltips "Light theme" / "Dark theme" (plus "Match the system" where Settings offers it), the words shell.bar.light / shell.bar.dark in en, es-MX and id and the old keys retired by bin/test-shell-lang.mjs; the navbar, the app bar, its More menu, Settings and the Studio hub use the one component. What stays: the stored values paper / model (localStorage cc-space, the cc_space cookie, data-space) and the brand file names cadcraft-logo-{paper,model}: internal names, nothing a person reads, so no stored setting breaks. What this does NOT touch: CadCraft's real paper space and model space (the drawing's layouts and the MODEL / PAPER status toggle), which keep their names because that is what they are. Help: the theme line in common/getting-started topics; the FAQ answer that names Paper and Model, if any, re-read. Owner addition the same day: in the apps the sun and moon are one row of two icon buttons inside the Craft menu, which opens from the app mark and name at the far left of the absolute top (UI.8, UI.24); icons in a row, never list items; the website navbar keeps its own pair.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftStudioSuperCad Studio App shellSite and services Planned 1 – docs/0.9-sessions/interface-wave1/SUMMARY.md
PlatformUI.21 Link to UI.21Comment on UI.21 One dock manager: a ribbon set becomes a palette and back; any tool group docks to a canvas edge, tabs, collapses, floats or takes a window

Owner 2026-11-09, on the reference at app.draftcraft.a Discord member: "the ability for a RIBBON set to turn into a palette, like in Adobe Photoshop or something similar, where you can dock panels/palettes, and in this particular case we can dock a ribbon to the top of the canvas." The idea: a ribbon group, a tool row, a toolbox group, Properties, the browser, the info box, the command line and a status strip are one thing, a DOCKABLE UNIT; a ribbon is units docked horizontally along the top of the canvas with a tab strip, a palette the same unit docked vertically in a side column, a tabbed panel several units in one slot, an icon rail a collapsed column, a floating palette a unit over the drawing, a window a unit on another monitor. Today each is its own block kind and code path (15 block kinds, four tear-off paths, the panel manager apart from the engine). Change: one dock manager in the shell (app-dock.js, grown from app-panels.js' dock / float / window logic and the engine's regions; SuperCad Studio's SC.2 panels, "everything is a panel that docks, tears off and groups", become a caller of it): zones = the four canvas edges plus the two panel columns, drawn as drop targets while dragging (UI.5); a unit dropped in a top or bottom zone lays out horizontally (the ribbon look), in a side zone vertically (icon + name rows, the palette look), on another unit it joins as a tab, on the rail it collapses to an icon, over the drawing it floats, past the window edge it tears off (UI.2/UI.3). The orientation belongs to the zone, the tools to the unit (format v2, UI.6), so the same Draw set is a ribbon group at the top and a palette at the side. The layout file says, per unit, which zone and in which order; the block kinds become presets of units in zones, so every built-in layout still reads and every adapter still fills them. Conformance: drag the Draw group from the ribbon to the left column and back in every app; a layout saved that way reloads the same.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftSuperCad Studio App shell Planned 8 UI.1 (the pattern) and UI.2 (one window service) first; UI.5 and UI.6 are built on this. docs/0.9-sessions/interface-wave1/SUMMARY.md
PlatformUI.22 Link to UI.22Comment on UI.22 The icon library: one drawing for every command, option and shell function in the eight apps, kept as data, the same in every layout and app

Owner 2026-11-09: "we need to create an entire library of icons (a task) that covers all of the application options, so that our icons are consistent regardless of the layout/app." What exists: app-workspace-icons.js holds 114 drawings on a 32-unit grid; about 270 specialist tools had no drawing by the 2026-11-05 count; the shell bar, More menu, panel tabs, status bar, menus and dialogs still use Bootstrap Icons (bi-*) and each app draws some of its own, so one command looks different from layout to layout and app to app. This row makes the library its own thing: (1) an inventory tool (tools/icons/inventory.mjs) reads every app's command registry, its toolbar, menu and toolbox definitions, the shell's bar buttons and panel tabs, Settings and the layout blocks, and writes docs/icons/INVENTORY.md: every command or option id, its apps, its drawing key or "missing"; nothing is drawn before the list exists; (2) the library in its own public repo (getcadcraft/icons, Apache-2.0, like every shared library): one SVG source per icon on the 32 grid with the five fills and two strokes as classes, names.json (key, name, aliases, the commands that use it), and a build that packs the set into app-workspace-icons.js, a sprite for the site and the add-ons kit, and the gallery data for UI.23; a hand edit of the packed file is refused by a check; (3) the drawings, under the UI.13 rules, in batches by app with the owner's look at each batch on the review sheet; shared verbs (move, copy, rotate, trim, offset, measure, zoom, undo) drawn once for every app; the shell's own set (home, find, help, settings, sun and moon, full screen, offline, problems, team, lessons, the AI and the panel buttons); (4) a lint in the release freeze: a registered command without a drawing, a drawing without a name, or a bi-* class left in shell or app chrome fails it; (5) the count in the release note (UI.18) comes from the inventory, never typed. Each app's own drawings are migrated or retired by its session.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftSuperCad Studio App shellApp codeTests and tooling Planned 10 UI.13 (the owner's drawing rules) first; UI.14 wires the result into every block. docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.23 Link to UI.23Comment on UI.23 Layout designer page in the developer section: how to build your own layout theme with the CadCraft icons, with the icon gallery and a layout checker

Owner 2026-11-09: "there should be a designer page, in the developer section, where people learn how to build their own layout theme with our icons." Today /developers is the API only (Overview, Guides, Reference, Console, Changelog, Your keys); layouts are documented for makers in the add-ons repo (docs/LAYOUTS.md) and the help topic common/layout-own, and nothing shows the icons. Change: /developers/layouts (SSR from Catalog and the library data, sub-nav entry "Layouts", in the sitemap and llms.txt): (1) the pattern: the zone map and the dockable-unit rule from UI.1 drawn live (the mockup's dock manager moved onto the site as a component), the block presets, what an adapter fills per app; (2) the icon gallery from the UI.22 build: every icon in Full colour, Outline colorized and the app's theme colour, with key, name and the commands that use it, search, copy-the-key, and the UI.13 drawing rules for anyone adding one; (3) the step-by-step guide: start from a built-in file, place units in zones, choose tool rows and their icons (toolrows.icons "set"), set the tone (UI.16), save as <name>.ccworkspace.json, install it in Settings › Layout › Add a layout file or in SuperCad Studio, or publish it as a layout add-on for everyone; (4) a layout checker: drop a layout file and the page runs the shared format module's resolveLayout in the browser (the engine's own code; progressive enhancement, the guide reads without it) and lists the notes: unknown places, blocks an app cannot fill, missing icon keys; (5) "Design it in the app": opens any app in /use in Arrange mode (UI.5) with one-click export of the result, so the page teaches and the app does; (6) "Themes people made": layout add-ons from the add-ons index, none until there are some. Words in en, es-MX, id; common/layout-own links the page; the 0.10 release note names it. Public and indexable; names no other program (vendor-names rule).

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftSuperCad Studio Site and servicesHelp and wordsTests and tooling Planned 4 UI.1 (the written pattern), UI.22 (gallery data); format v2 (UI.6) when it lands, v1 until then. docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.24 Link to UI.24Comment on UI.24 The Craft menu at the far right of the absolute top and Add-ons at the far right of the menu bar: the shell's own buttons condensed, pinnable

Owner 2026-11-09: condense "OUR buttons" that extend the site's functions; an absolute top with the logo, the app name, a dock space for the menu bar "and then the CRAFT MENU (far right): settings, full screen, help, support, about, login/logout, account. ADD-ONS goes into the top menu far right and lists all the add-ons and panels"; "some are addon panels, others are part of CRAFT MENU". What exists: app-bar.inc draws the mark, area chip, theme toggle, Find, Help, Save for offline, Settings, Full screen; thirteen modules each insert their own button before Settings. Change: (1) one registration, ccShell.option(), that every module and add-on calls instead of inserting a button; (2) the Craft menu opens from the app mark and name at the far left of the absolute top (UI.8), the same in every app and layout: Settings, Full screen, Help, Support, About (with Report a problem), Save for offline, Visit GetCadCraft.com, the sun and moon as one row of icons (UI.20), Sign in, then Account with sign out inside; (3) Add-ons as the last item of the menu bar in every layout that has one (a title-bar button where none does): every add-on and panel (module, Team, AI and add-on panels), each a dockable unit (UI.21) opening at its remembered place, a tick when open; closing a panel returns it to this list; (4) Find (owner): [ search box | magnifying glass ] at the far right of the absolute top; the box folds to the glass when room is short and a click opens it; Ctrl+K; the area chip stays beside the app name; Problems also keeps its count in the status bar; (5) up to six options pinned in the absolute top as buttons, saved per app and in the layout file (format v2); (6) badges on a pinned button or on the mark; (7) Alt+O opens the Craft menu; options keep their shortcuts. The app's tools are not touched. Words shell.craft.*; help craft-menu; test: every option reachable in every app and layout, no bar button outside the registry.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftSuperCad Studio App shell Planned 4 UI.8 (the title bar block) agrees on where the menu sits; UI.21 makes it a dockable unit. docs/0.9-sessions/interface-wave1/SUMMARY.md
PlatformUI.25 Link to UI.25Comment on UI.25 One state language: an action enables or disables menu items, shows and updates panels, and selects on the canvas; every unit follows the same state

Owner 2026-11-09: "remember that actions can enable or disable menu items or make panels show/update as well as selecting objects on the canvas." What exists: every app wires this by hand in its own code: Properties refreshes on the app's selection event, the project tree selects through app code, the command registry (ccInput) knows commands but no enabled or visible condition, menus and toolbars are static until an app toggles classes, contextual ribbon tabs do not exist (UI.11), Problems and the add-ons read their own events. Change: (1) one state in the shell (ccState): selection (ids, kinds, count), mode (model / paper, 2D / 3D), the active view, the current layer and tool, document dirty; the apps publish into it through the host adapter; nothing else writes it; (2) declarations as data: a command, menu item, button, ribbon tab or panel says when it is enabled, visible or shown (enabled: selection.count > 0; visible: selection.kind == wall; show: problems.count > 0) in the registry and the layout file (format v2), never in per-app DOM code; the dock manager applies them: items dim, contextual tabs appear and go (UI.11), panels show or refresh, badges count; (3) the reverse channel: a unit acts on the canvas through the host (select, reveal, highlight), so a Browser row, a Problems line, a schedule row or a search hit selects and zooms to the objects with the same undoable selection the pointer makes; (4) one action path: every button, menu item and shortcut runs the registry command, so one command in a ribbon, a palette, a menu and the status bar has one enabled state and one label; (5) a check (bin/test-shell-state.mjs) opens every app, selects through the host and asserts Modify enabled, Properties updated, the Browser row lit, a declared contextual tab shown, then selects from the panel and asserts the canvas selection. The mockup's demo shows the behaviour (click a wall, or its Browser row). SuperCad Studio's host is the same contract.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftSuperCad Studio App shellApp code Planned 5 UI.21 (units) and UI.6 (format v2) carry the declarations; each app publishes its state in its own session. docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.26 Link to UI.26Comment on UI.26 One head on every panel and ribbon group: grab handle far left, close far right, and the handle opens the dock menu (Dock to, Float, Window, Pin, Hide)

Owner 2026-11-09: "we should make the grab handle and close x standardized on all panels and ribbons (far left, far right), and a drop-down menu can be click the grab handle (menu: dock to xxx, pin)." What exists: the two app panels have the shell's grip bar (app-panels.js) and Problems a "…" menu (dock right / left, float, window); the ribbon has its own tear-off handle; the toolbox, tool rows and status bar have popout buttons in different corners; the Vector and Sketch palettes draw their own heads. Change: one head component in the dock manager (UI.21) for every unit in every zone and orientation: the grab handle at the far left (drag = move; click = the dock menu), the unit's tabs or name in the middle, collapse, and close at the far right, in that order, always. The dock menu lists Dock to top / canvas top / left panel / right panel / bottom (only the zones the layout allows, UI.1), Float on the drawing, Open in a window (UI.2, UI.3), Pin (the unit stays put: drag, auto-hide and close are off until unpinned), Collapse / Open, Hide (place none; it comes back from Settings › Layout › Hidden or the Craft options menu, UI.24), then the unit's own commands if it registers any. Right-click and a long press open the same menu; the handle is focusable. Head height, glyphs (from the icon library, UI.22) and colours (the zone's family) come from the tokens, so a ribbon group's head and a palette's head differ only in orientation. The ribbon's tear-off handle, the popout buttons and the "…" menus retire into it. Conformance: every unit in every layout shows handle left and close right, the menu opens, and each item does what it says. Owner addition the same day: a visibility order on the head so the close never draws the eye: title or tab name 100 % of the ink, grab handle 70 %, collapse 55 %, close 40 %, full ink on hover and focus; four tokens in app-workspace.css, the same on both themes.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftSuperCad Studio App shell Planned 2 UI.21 (the unit) is where the head lives. docs/0.9-sessions/interface-wave1/SUMMARY.md
CivilCraft 1
FeatureCV.1 Link to CV.1Comment on CV.1 BCF issues in CivilCraft, next to LandXML

Asked on LinkedIn under a post about CivilCraft, by a BIM/COBie specialist: is there BCF support, or similar, when using the LandXML interoperability with site personnel? Today: no. LandXML 1.2 in/out is built (points, TIN surfaces, alignments with profiles, pipe networks) but LandXML has no place for issues. BCF 2.1 is built and schema-valid in BimCraft (Issues, one viewpoint per issue) and MarkupCraft (PDF markups). This row: Issues in CivilCraft: pin a problem to a plan location, an alignment station, a surface point or a pipe, with its 2D/3D view, a title, status and comment; export and import BCF 2.1 (and read 3.0) so site personnel and BIM tools exchange them next to the LandXML file. Build on BimCraft's BCF code as shared lib plus a thin CivilCraft adapter (build-for-super), and tie into Problems where a check already finds the issue. Open: how a BCF viewpoint maps to a plan or profile view (BCF cameras are 3D; the plan view needs an orthogonal camera); whether issues travel inside the LandXML as extension data (not standard: probably a sidecar). Untested: nothing built.

CivilCraft Planned 2 – LinkedIn comment on the CivilCraft post, 2026-11-09
Suggest an item

Anyone can send one; no account and no name needed. The team reads each idea, and the ones we take on appear on this page. Please leave out web addresses.