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.
Files changed (66) hide show
  1. package/README.md +44 -12
  2. package/dist/cli.js +98 -23
  3. package/dist/graph/engine.js +4 -1
  4. package/dist/graph/params.d.ts +7 -1
  5. package/dist/graph/params.js +21 -8
  6. package/dist/graph/types.d.ts +11 -2
  7. package/dist/host/export_scene.py +200 -0
  8. package/dist/host/node.d.ts +8 -0
  9. package/dist/host/node.js +114 -0
  10. package/dist/host/types.d.ts +5 -0
  11. package/dist/inspect.js +5 -2
  12. package/dist/io/common.d.ts +6 -0
  13. package/dist/io/export.js +7 -4
  14. package/dist/io/gltf.d.ts +1 -1
  15. package/dist/io/gltf.js +167 -32
  16. package/dist/io/heightmap.d.ts +1 -1
  17. package/dist/io/import.d.ts +2 -2
  18. package/dist/io/import.js +10 -3
  19. package/dist/io/png-decode.d.ts +5 -2
  20. package/dist/io/png-decode.js +10 -3
  21. package/dist/io/textures.d.ts +31 -2
  22. package/dist/io/textures.js +66 -18
  23. package/dist/io/unreal.js +22 -5
  24. package/dist/mcp/docs.d.ts +9 -2
  25. package/dist/mcp/docs.js +113 -2
  26. package/dist/mcp/inputs.d.ts +28 -0
  27. package/dist/mcp/inputs.js +377 -0
  28. package/dist/mcp/server.js +4 -1
  29. package/dist/mcp/tools.js +32 -18
  30. package/dist/nodes/code.js +2 -0
  31. package/dist/nodes/copy.js +14 -4
  32. package/dist/nodes/flow.js +136 -35
  33. package/dist/nodes/heightfield.js +67 -11
  34. package/dist/nodes/texture.d.ts +1 -1
  35. package/dist/nodes/texture.js +61 -5
  36. package/dist/render/options.js +2 -1
  37. package/dist/render/raster.d.ts +2 -0
  38. package/dist/render/raster.js +3 -3
  39. package/dist/render/texview.d.ts +9 -1
  40. package/dist/render/texview.js +43 -2
  41. package/dist/viewer/app/api.js +64 -0
  42. package/dist/viewer/app/camera.js +230 -0
  43. package/dist/viewer/app/grid.js +217 -0
  44. package/dist/viewer/app/guides.js +402 -0
  45. package/dist/viewer/app/keymap.js +187 -0
  46. package/dist/viewer/app/layers.js +225 -0
  47. package/dist/viewer/app/main.js +590 -0
  48. package/dist/viewer/app/markers.js +206 -0
  49. package/dist/viewer/app/mesh.js +172 -0
  50. package/dist/viewer/app/outliner.js +184 -0
  51. package/dist/viewer/app/props.js +432 -0
  52. package/dist/viewer/app/scene.js +529 -0
  53. package/dist/viewer/app/select.js +142 -0
  54. package/dist/viewer/app/selection.js +79 -0
  55. package/dist/viewer/app/style.css +172 -0
  56. package/dist/viewer/app/undo.js +49 -0
  57. package/dist/viewer/app/units.js +51 -0
  58. package/dist/viewer/app/vars.js +64 -0
  59. package/dist/viewer/index.html +53 -184
  60. package/dist/viewer/server.d.ts +42 -1
  61. package/dist/viewer/server.js +432 -17
  62. package/dist/workspace.d.ts +52 -2
  63. package/dist/workspace.js +95 -3
  64. package/package.json +8 -3
  65. package/shelf/brick_wall.json +1 -1
  66. 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(dir, engine) {
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
- for (const d of [this.dir, this.graphDir, this.assetDir, this.exportDir])
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.1.0",
4
- "description": "A procedural geometry engine: Houdini-style node graphs, headless, deterministic, exports to Unreal, Blender and Houdini",
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
  }
@@ -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": 12, "desc": "length along X (m)" },
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",
@@ -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": 20, "desc": "length along X (m)" },
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": 10, "desc": "number of window openings" },
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,