topotopo 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -12
- package/dist/cli.js +98 -23
- package/dist/graph/engine.js +4 -1
- package/dist/graph/params.d.ts +7 -1
- package/dist/graph/params.js +21 -8
- package/dist/graph/types.d.ts +11 -2
- package/dist/host/export_scene.py +200 -0
- package/dist/host/node.d.ts +8 -0
- package/dist/host/node.js +114 -0
- package/dist/host/types.d.ts +5 -0
- package/dist/inspect.js +5 -2
- package/dist/io/common.d.ts +6 -0
- package/dist/io/export.js +7 -4
- package/dist/io/gltf.d.ts +1 -1
- package/dist/io/gltf.js +167 -32
- package/dist/io/heightmap.d.ts +1 -1
- package/dist/io/import.d.ts +2 -2
- package/dist/io/import.js +10 -3
- package/dist/io/png-decode.d.ts +5 -2
- package/dist/io/png-decode.js +10 -3
- package/dist/io/textures.d.ts +31 -2
- package/dist/io/textures.js +66 -18
- package/dist/io/unreal.js +22 -5
- package/dist/mcp/docs.d.ts +9 -2
- package/dist/mcp/docs.js +113 -2
- package/dist/mcp/inputs.d.ts +28 -0
- package/dist/mcp/inputs.js +377 -0
- package/dist/mcp/server.js +4 -1
- package/dist/mcp/tools.js +32 -18
- package/dist/nodes/code.js +2 -0
- package/dist/nodes/copy.js +14 -4
- package/dist/nodes/flow.js +136 -35
- package/dist/nodes/heightfield.js +67 -11
- package/dist/nodes/texture.d.ts +1 -1
- package/dist/nodes/texture.js +61 -5
- package/dist/render/options.js +2 -1
- package/dist/render/raster.d.ts +2 -0
- package/dist/render/raster.js +3 -3
- package/dist/render/texview.d.ts +9 -1
- package/dist/render/texview.js +43 -2
- package/dist/viewer/app/api.js +64 -0
- package/dist/viewer/app/camera.js +230 -0
- package/dist/viewer/app/grid.js +217 -0
- package/dist/viewer/app/guides.js +402 -0
- package/dist/viewer/app/keymap.js +187 -0
- package/dist/viewer/app/layers.js +225 -0
- package/dist/viewer/app/main.js +590 -0
- package/dist/viewer/app/markers.js +206 -0
- package/dist/viewer/app/mesh.js +172 -0
- package/dist/viewer/app/outliner.js +184 -0
- package/dist/viewer/app/props.js +432 -0
- package/dist/viewer/app/scene.js +529 -0
- package/dist/viewer/app/select.js +142 -0
- package/dist/viewer/app/selection.js +79 -0
- package/dist/viewer/app/style.css +172 -0
- package/dist/viewer/app/undo.js +49 -0
- package/dist/viewer/app/units.js +51 -0
- package/dist/viewer/app/vars.js +64 -0
- package/dist/viewer/index.html +53 -184
- package/dist/viewer/server.d.ts +42 -1
- package/dist/viewer/server.js +432 -17
- package/dist/workspace.d.ts +52 -2
- package/dist/workspace.js +95 -3
- package/package.json +8 -3
- package/shelf/brick_wall.json +1 -1
- package/shelf/wall_segment.json +2 -2
package/dist/workspace.js
CHANGED
|
@@ -1,11 +1,37 @@
|
|
|
1
|
-
// A workspace: named graphs (persisted as JSON files), saved assets, and the engine
|
|
1
|
+
// A workspace: named graphs (persisted as JSON files), saved assets, the human's input files, and the engine
|
|
2
2
|
// that cooks them. Shared by the MCP server, the live viewer and the CLI.
|
|
3
|
+
// <dir>/graphs/<name>.json graphs <dir>/assets/<name>.json saved assets
|
|
4
|
+
// <dir>/inputs/… files people drop in (meshes, guide curves, PNG masks), read with file nodes; or a
|
|
5
|
+
// folder given to the constructor (`topotopo view`: inputs/ beside the graph file)
|
|
6
|
+
// <dir>/exports/… export output
|
|
3
7
|
import fs from 'node:fs';
|
|
4
8
|
import path from 'node:path';
|
|
5
9
|
import { Engine, wiringOf } from './graph/engine.js';
|
|
6
10
|
import { didYouMean, renameExprRefs } from './graph/params.js';
|
|
7
11
|
import { migrateGraph } from './graph/migrate.js';
|
|
8
12
|
const NAME_RE = /^[A-Za-z_][\w.-]*$/;
|
|
13
|
+
const GEOMETRY_EXTS = new Set(['obj', 'stl', 'ply', 'glb', 'gltf', 'geo', 'bgeo', 'blend', 'csv', 'json', 'points.json']);
|
|
14
|
+
const IMAGE_EXTS = new Set(['png']);
|
|
15
|
+
/** Lower-case extension of a file name without the dot; ".points.json" is one extension, dot-files have none. */
|
|
16
|
+
export function inputExt(file) {
|
|
17
|
+
const b = file.toLowerCase().replace(/^.*[\\/]/, '');
|
|
18
|
+
if (b.endsWith('.points.json') && b.length > '.points.json'.length)
|
|
19
|
+
return 'points.json';
|
|
20
|
+
const i = b.lastIndexOf('.');
|
|
21
|
+
return i > 0 ? b.slice(i + 1) : '';
|
|
22
|
+
}
|
|
23
|
+
export function inputKind(ext) {
|
|
24
|
+
return GEOMETRY_EXTS.has(ext) ? 'geometry' : IMAGE_EXTS.has(ext) ? 'image' : 'other';
|
|
25
|
+
}
|
|
26
|
+
/** Thrown by Workspace.edit when the edited graph does not validate; the stored graph is unchanged. */
|
|
27
|
+
export class EditRejected extends Error {
|
|
28
|
+
errors;
|
|
29
|
+
constructor(errors) {
|
|
30
|
+
super(`✗ edits rejected (graph unchanged) — ${errors.length} error(s):\n` + errors.map((e) => ` [${e.node}] ${e.message}`).join('\n'));
|
|
31
|
+
this.name = 'EditRejected';
|
|
32
|
+
this.errors = errors;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
9
35
|
export class Workspace {
|
|
10
36
|
dir;
|
|
11
37
|
engine;
|
|
@@ -13,16 +39,26 @@ export class Workspace {
|
|
|
13
39
|
versions = new Map();
|
|
14
40
|
lastReport = new Map();
|
|
15
41
|
listeners = new Set();
|
|
16
|
-
constructor(
|
|
42
|
+
/** the inputs folder given to the constructor (WorkspaceOptions.inputs), if any */
|
|
43
|
+
inputsAt;
|
|
44
|
+
constructor(dir, engine, opts = {}) {
|
|
17
45
|
this.dir = path.resolve(dir);
|
|
18
46
|
this.engine = engine;
|
|
47
|
+
this.inputsAt = opts.inputs === undefined ? undefined : path.resolve(opts.inputs);
|
|
19
48
|
engine.graphResolver = (n) => this.graphs.get(n);
|
|
20
49
|
}
|
|
21
50
|
get graphDir() { return path.join(this.dir, 'graphs'); }
|
|
22
51
|
get assetDir() { return path.join(this.dir, 'assets'); }
|
|
23
52
|
get exportDir() { return path.join(this.dir, 'exports'); }
|
|
53
|
+
/**
|
|
54
|
+
* files people drop in: meshes, guide curves and points, PNG masks and heightmaps (read as "inputs/<path>");
|
|
55
|
+
* <dir>/inputs unless the constructor was given another folder
|
|
56
|
+
*/
|
|
57
|
+
get inputDir() { return this.inputsAt ?? path.join(this.dir, 'inputs'); }
|
|
24
58
|
load() {
|
|
25
|
-
|
|
59
|
+
// the workspace's own inputs/ is made at once (people open it to drop files in); a folder given to the constructor
|
|
60
|
+
// is made by the first file written into it
|
|
61
|
+
for (const d of [this.dir, this.graphDir, this.assetDir, this.exportDir, ...(this.inputsAt ? [] : [this.inputDir])])
|
|
26
62
|
fs.mkdirSync(d, { recursive: true });
|
|
27
63
|
const assetErrors = [];
|
|
28
64
|
for (const f of fs.readdirSync(this.assetDir).filter((f) => f.endsWith('.json'))) {
|
|
@@ -86,6 +122,41 @@ export class Workspace {
|
|
|
86
122
|
this.graphs.delete(name);
|
|
87
123
|
this.persist(name);
|
|
88
124
|
}
|
|
125
|
+
/** Every file under inputs/, recursively: posix paths relative to it, sorted. Dot-files and dot-folders are skipped. */
|
|
126
|
+
listInputs() {
|
|
127
|
+
const out = [];
|
|
128
|
+
const walk = (dir, rel, depth) => {
|
|
129
|
+
let entries;
|
|
130
|
+
try {
|
|
131
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
for (const e of entries) {
|
|
137
|
+
if (e.name.startsWith('.'))
|
|
138
|
+
continue;
|
|
139
|
+
const abs = path.join(dir, e.name), r = rel ? `${rel}/${e.name}` : e.name;
|
|
140
|
+
let st;
|
|
141
|
+
try {
|
|
142
|
+
st = fs.statSync(abs);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
continue;
|
|
146
|
+
} // a broken link
|
|
147
|
+
if (st.isDirectory()) {
|
|
148
|
+
if (depth < 32)
|
|
149
|
+
walk(abs, r, depth + 1);
|
|
150
|
+
} // the depth cap stops link cycles
|
|
151
|
+
else if (st.isFile()) {
|
|
152
|
+
const ext = inputExt(e.name);
|
|
153
|
+
out.push({ path: r, kind: inputKind(ext), ext, size: st.size, mtime: Math.round(st.mtimeMs) });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
walk(this.inputDir, '', 0);
|
|
158
|
+
return out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
159
|
+
}
|
|
89
160
|
/** Merge nodes/vars/output of `patch` into an existing graph (upsert). */
|
|
90
161
|
mergeGraph(name, patch) {
|
|
91
162
|
patch = migrateGraph(patch);
|
|
@@ -261,6 +332,27 @@ export class Workspace {
|
|
|
261
332
|
}
|
|
262
333
|
return { graph: g, notes };
|
|
263
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* The one path for an edit, whoever makes it (the MCP edit tool, the viewer's controls): apply the ops, validate
|
|
337
|
+
* (an invalid result throws EditRejected listing the per-node errors and stores nothing), store, cook at `frame`,
|
|
338
|
+
* then notify the listeners. They hear after the cook so that what they read (lastReport, cached geometry) is the
|
|
339
|
+
* new version's; a cook that throws still notifies, since the graph was stored.
|
|
340
|
+
*/
|
|
341
|
+
async edit(name, ops, opts = {}) {
|
|
342
|
+
const { graph, notes } = this.applyEdits(name, ops);
|
|
343
|
+
const v = this.engine.validate(graph);
|
|
344
|
+
if (v.errors.length)
|
|
345
|
+
throw new EditRejected(v.errors);
|
|
346
|
+
this.setGraph(name, graph);
|
|
347
|
+
let report;
|
|
348
|
+
try {
|
|
349
|
+
report = await this.cook(name, undefined, opts.frame);
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
this.bump(name);
|
|
353
|
+
}
|
|
354
|
+
return { graph: this.getGraph(name), notes, report };
|
|
355
|
+
}
|
|
264
356
|
async cook(name, target, frame) {
|
|
265
357
|
const g = this.getGraph(name);
|
|
266
358
|
const r = await this.engine.cook(g, { target, frame });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "topotopo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "An open-source procedural geometry engine: node graphs in JSON, cooked headless and deterministic, exported to Unreal, Blender and Houdini. Built for LLM agents through MCP, and for people through the CLI.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"mcp": "node src/mcp/server.ts",
|
|
39
39
|
"preview": "node scripts/preview.ts --examples",
|
|
40
40
|
"cook": "node src/cli.ts",
|
|
41
|
-
"test": "node test/smoke.ts && node test/golden.ts && node test/property.ts && node test/fuzz.ts && node test/docs.ts && node test/portability.ts && node test/examples.ts && node scripts/gen-notice.ts --check && node test/io.ts && node test/interop.ts && node test/mcp.ts && node test/engine.ts && node test/topology.ts && node test/deform.ts && node test/cloth.ts && node test/arrays.ts && node test/bugs.ts && node test/uv.ts && node test/polyedit.ts && node test/bevel.ts && node test/measure.ts && node test/render.ts && node test/packed.ts && node test/heightfield.ts && node test/remesh.ts && node test/pcg.ts && node test/scatterkernel.ts && node test/splines.ts && node test/shelf.ts && node test/textures.ts && node test/pool.ts && node test/kernelsloader.ts && node test/kernels.ts && node test/sdfgrid.ts && node test/remeshquad.ts && node test/erosionkernel.ts && node test/erosionsimd.ts && node test/particles.ts && node test/rbd.ts && node test/limits.ts",
|
|
41
|
+
"test": "node test/smoke.ts && node test/golden.ts && node test/property.ts && node test/fuzz.ts && node test/docs.ts && node test/portability.ts && node test/examples.ts && node scripts/gen-notice.ts --check && node test/io.ts && node test/import.ts && node test/interop.ts && node test/mcp.ts && node test/engine.ts && node test/viewer.ts && node test/viewer-logic.ts && node test/topology.ts && node test/deform.ts && node test/cloth.ts && node test/arrays.ts && node test/bugs.ts && node test/uv.ts && node test/polyedit.ts && node test/bevel.ts && node test/measure.ts && node test/render.ts && node test/packed.ts && node test/heightfield.ts && node test/remesh.ts && node test/pcg.ts && node test/scatterkernel.ts && node test/splines.ts && node test/shelf.ts && node test/textures.ts && node test/pool.ts && node test/kernelsloader.ts && node test/kernels.ts && node test/sdfgrid.ts && node test/remeshquad.ts && node test/erosionkernel.ts && node test/erosionsimd.ts && node test/particles.ts && node test/rbd.ts && node test/limits.ts",
|
|
42
42
|
"test:limits": "node test/limits.ts",
|
|
43
43
|
"kernels:build": "node scripts/kernels-build.ts",
|
|
44
44
|
"test:kernels": "node test/scatterkernel.ts && node test/kernelsloader.ts && node test/kernels.ts && node test/sdfgrid.ts && node test/remeshquad.ts && node test/erosionkernel.ts && node test/erosionsimd.ts",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"test:portability": "node test/portability.ts",
|
|
64
64
|
"test:examples": "node test/examples.ts",
|
|
65
65
|
"test:interop": "node test/interop.ts",
|
|
66
|
+
"test:viewer": "node test/viewer.ts && node test/viewer-logic.ts",
|
|
66
67
|
"showcase": "node test/showcase.ts",
|
|
67
68
|
"export:demo": "node scripts/export-demo.ts",
|
|
68
69
|
"demo:footage": "node scripts/demo-footage.ts",
|
|
@@ -124,5 +125,9 @@
|
|
|
124
125
|
"bugs": {
|
|
125
126
|
"url": "https://github.com/topotopo-dev/topotopo/issues"
|
|
126
127
|
},
|
|
128
|
+
"funding": {
|
|
129
|
+
"type": "github",
|
|
130
|
+
"url": "https://github.com/sponsors/faxlab"
|
|
131
|
+
},
|
|
127
132
|
"author": "faxcorp <hello@topotopo.dev>"
|
|
128
133
|
}
|
package/shelf/brick_wall.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"version": 1,
|
|
3
3
|
"description": "Brick wall from x = 0 to x = length: individual bricks in running, stack, English or Flemish bond, cut to the wall ends and top, with random offset, yaw and tint, standing slightly proud of a recessed mortar core. One wythe deep (headers are drawn as half bricks). Pivot at the start of the wall, on the ground, centred in thickness. Pieces: brick_wall_bricks (brick), brick_wall_mortar (mortar). Every piece is a closed box.",
|
|
4
4
|
"params": {
|
|
5
|
-
"length": { "type": "float", "default": 3, "min": 0.3, "max":
|
|
5
|
+
"length": { "type": "float", "default": 3, "min": 0.3, "max": 40, "desc": "length along X (m)" },
|
|
6
6
|
"height": { "type": "float", "default": 1.8, "min": 0.1, "max": 5, "desc": "height (m)" },
|
|
7
7
|
"brick_length": {
|
|
8
8
|
"type": "float",
|
package/shelf/wall_segment.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 1,
|
|
3
3
|
"description": "Straight wall from x = 0 to x = length with window (and an optional door) openings cut by a boolean, filled with the window_frame and door_frame shelf assets. The openings are spread in equal bays; the door takes the middle bay. Modular: segments placed end to end line up. Pivot at the start of the wall, on the ground, centred in thickness (+Z = outside). Pieces: wall (plaster) plus the frames' pieces. Every piece is closed (the wall is one watertight solid).",
|
|
4
4
|
"params": {
|
|
5
|
-
"length": { "type": "float", "default": 4, "min": 0.5, "max":
|
|
5
|
+
"length": { "type": "float", "default": 4, "min": 0.5, "max": 60, "desc": "length along X (m)" },
|
|
6
6
|
"height": { "type": "float", "default": 3, "min": 1, "max": 10, "desc": "height (m)" },
|
|
7
7
|
"thickness": { "type": "float", "default": 0.25, "min": 0.05, "max": 1, "desc": "thickness along Z (m)" },
|
|
8
|
-
"windows": { "type": "int", "default": 2, "min": 0, "max":
|
|
8
|
+
"windows": { "type": "int", "default": 2, "min": 0, "max": 24, "desc": "number of window openings" },
|
|
9
9
|
"window_width": {
|
|
10
10
|
"type": "float",
|
|
11
11
|
"default": 1,
|