/ concept-collection / mesh-studio
concept-collection / mesh-studio
Add Random CAD model source drawing from abc-step-1000
Fetches a random file (<= 2 MB) from the rehosted first-1000 slice of the ABC dataset, gunzips it client-side, and feeds it to the normal STEP import path. Attribution footnote under the source buttons.
Jeremy Magland <jmagland@flatironinstitute.org> committed commit bd00eac973e6 parent 8a92adc Browse files
4 changed files+98−1
CLAUDE.mdmodified+6−0View file
@@ -23,6 +23,10 @@ src/render/ SurfaceView.tsx: plain three.js, one mesh per patch (for picking)
2323 src/export/ meshWriters.ts (OBJ/PLY/STL, dependency-free), nurbsJson.ts.
2424 src/App.tsx sidebar + viewport; keeps the OpenCascade instance and the
2525 (NURBS-converted) mesh shape in refs for re-tessellation.
26+src/abcDataset.ts "Random CAD model" source: fetches index.json from
27+ https://concept-collection.github.io/abc-step-1000/ (first 1000
28+ ABC-dataset STEP files, gzip-served), picks a random file ≤2 MB,
29+ gunzips via DecompressionStream, hands bytes to importCadFile.
2630 ```
2731
2832 ## Key gotchas
@@ -56,6 +60,8 @@ bundling (including the OCCT wasm asset). Then `npm run dev` and:
5660 - drag the resolution slider (faceting should visibly change);
5761 - click a face → the inspector shows degree / poles / knots;
5862 - "Sample STEP" and a real uploaded STEP/IGES both render;
63+- "Random CAD model" downloads from abc-step-1000 and renders (needs that
64+ Pages site up, and the network);
5965 - export OBJ/PLY/STL/NURBS-JSON/STEP.
6066
6167 Not yet deployed to the org Pages site (same procedure as mesh-converter /
README.mdmodified+4−0View file
@@ -30,6 +30,10 @@ extracts those polynomial patches (control points, weights, knots, degree)
3030 - **STEP / IGES import**: open a `.step`/`.stp` or `.iges`/`.igs` file — OCCT
3131 reads it natively. "Sample STEP" round-trips a box through OCCT's own writer +
3232 reader to demonstrate the import path with no bundled file.
33+- **Random CAD model**: downloads a random real-world model from
34+ [abc-step-1000](https://concept-collection.github.io/abc-step-1000/), a
35+ rehosted slice of the [ABC dataset](https://deep-geometry.github.io/abc-dataset/)
36+ of CAD models (Koch et al., CVPR 2019).
3337
3438 ## Export
3539
src/App.tsxmodified+36−1View file
@@ -11,6 +11,7 @@ import { importCadFile } from './occ/importCad'
1111 import { shapeToStep } from './occ/exportCad'
1212 import { makeBox } from './occ/primitives'
1313 import { primitives } from './sources'
14+import { ABC_DATASET_URL, fetchRandomAbcStep } from './abcDataset'
1415 import { toOBJ, toPLY, toSTL } from './export/meshWriters'
1516 import { toNurbsJson } from './export/nurbsJson'
1617 import './index.css'
@@ -114,6 +115,23 @@ function App() {
114115 )
115116 }
116117
118+ const loadRandomAbc = async () => {
119+ setError(null)
120+ setBusy('downloading')
121+ try {
122+ const { name, bytes } = await fetchRandomAbcStep()
123+ await load(
124+ (oc) => importCadFile(oc, name, bytes).shape,
125+ { kind: 'step', label: `${name} (ABC dataset)` },
126+ name.replace(/\.[^.]+$/, ''),
127+ { format: 'step', bytes },
128+ )
129+ } catch (e) {
130+ setError(e instanceof Error ? e.message : String(e))
131+ setBusy(null)
132+ }
133+ }
134+
117135 const openFile = async (file: File) => {
118136 const bytes = new Uint8Array(await file.arrayBuffer())
119137 const lower = file.name.toLowerCase()
@@ -211,7 +229,20 @@ function App() {
211229 <button onClick={loadSampleStep} disabled={busy !== null}>
212230 Sample STEP
213231 </button>
232+ <button
233+ onClick={() => void loadRandomAbc()}
234+ disabled={busy !== null}
235+ title="Download a random CAD model from the first 1000 STEP files of the ABC dataset"
236+ >
237+ Random CAD model
238+ </button>
214239 </div>
240+ <p className="footnote">
241+ Random models are drawn from{' '}
242+ <a href="https://concept-collection.github.io/abc-step-1000/">abc-step-1000</a>, a
243+ rehosted slice of the <a href={ABC_DATASET_URL}>ABC dataset</a> of CAD models (Koch et
244+ al., CVPR 2019).
245+ </p>
215246 <input
216247 ref={fileInputRef}
217248 type="file"
@@ -223,7 +254,11 @@ function App() {
223254 e.target.value = ''
224255 }}
225256 />
226- {busy && <div className="busy">{busy === 'building' ? 'Building model…' : 'Re-meshing…'}</div>}
257+ {busy && (
258+ <div className="busy">
259+ {busy === 'building' ? 'Building model…' : busy === 'downloading' ? 'Downloading model…' : 'Re-meshing…'}
260+ </div>
261+ )}
227262 {error && <div className="error">{error}</div>}
228263 </section>
229264
src/abcDataset.tsadded+52−0View file
@@ -0,0 +1,52 @@
1+/**
2+ * Random STEP models from abc-step-1000 — the first 1000 STEP files of the
3+ * ABC CAD dataset (Koch et al., CVPR 2019), rehosted gzip-compressed at
4+ * https://concept-collection.github.io/abc-step-1000/. No OCCT here: this
5+ * module only fetches bytes; the app feeds them to the normal import path.
6+ */
7+
8+export const ABC_BASE = 'https://concept-collection.github.io/abc-step-1000'
9+export const ABC_DATASET_URL = 'https://deep-geometry.github.io/abc-dataset/'
10+
11+// Files above this size make OCCT churn for a long time (the collection's
12+// largest is 204 MB); random picks stay snappy by drawing from the rest.
13+const MAX_STEP_BYTES = 2_000_000
14+
15+interface AbcIndexFile {
16+ id: string
17+ name: string
18+ path: string
19+ stepBytes: number
20+ gzBytes: number
21+}
22+
23+let indexPromise: Promise<AbcIndexFile[]> | null = null
24+
25+function fetchIndex(): Promise<AbcIndexFile[]> {
26+ indexPromise ??= fetch(`${ABC_BASE}/index.json`)
27+ .then((res) => {
28+ if (!res.ok) throw new Error(`abc-step-1000 index: HTTP ${res.status}`)
29+ return res.json()
30+ })
31+ .then((index: { files: AbcIndexFile[] }) => index.files)
32+ .catch((e) => {
33+ indexPromise = null // allow retry after a transient failure
34+ throw e
35+ })
36+ return indexPromise
37+}
38+
39+async function gunzipIfNeeded(buf: ArrayBuffer): Promise<Uint8Array> {
40+ const head = new Uint8Array(buf, 0, 2)
41+ if (head[0] !== 0x1f || head[1] !== 0x8b) return new Uint8Array(buf)
42+ const stream = new Blob([buf]).stream().pipeThrough(new DecompressionStream('gzip'))
43+ return new Uint8Array(await new Response(stream).arrayBuffer())
44+}
45+
46+export async function fetchRandomAbcStep(): Promise<{ name: string; bytes: Uint8Array }> {
47+ const files = (await fetchIndex()).filter((f) => f.stepBytes <= MAX_STEP_BYTES)
48+ const file = files[Math.floor(Math.random() * files.length)]
49+ const res = await fetch(`${ABC_BASE}/${file.path}`)
50+ if (!res.ok) throw new Error(`${file.name}: HTTP ${res.status}`)
51+ return { name: file.name, bytes: await gunzipIfNeeded(await res.arrayBuffer()) }
52+}