topotopo 0.1.0 → 0.3.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 (80) hide show
  1. package/README.md +61 -12
  2. package/dist/cli.js +161 -23
  3. package/dist/graph/engine.js +18 -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 +28 -2
  7. package/dist/host/artifacts.d.ts +21 -0
  8. package/dist/host/artifacts.js +58 -0
  9. package/dist/host/export-targets.d.ts +33 -0
  10. package/dist/host/export-targets.js +84 -0
  11. package/dist/host/export_scene.py +200 -0
  12. package/dist/host/node.d.ts +8 -0
  13. package/dist/host/node.js +114 -0
  14. package/dist/host/types.d.ts +5 -0
  15. package/dist/inspect.js +5 -2
  16. package/dist/io/common.d.ts +6 -0
  17. package/dist/io/export.js +7 -4
  18. package/dist/io/gltf.d.ts +1 -1
  19. package/dist/io/gltf.js +167 -32
  20. package/dist/io/heightmap.d.ts +1 -1
  21. package/dist/io/import.d.ts +2 -2
  22. package/dist/io/import.js +10 -3
  23. package/dist/io/png-decode.d.ts +5 -2
  24. package/dist/io/png-decode.js +10 -3
  25. package/dist/io/targets.d.ts +31 -0
  26. package/dist/io/targets.js +75 -0
  27. package/dist/io/textures.d.ts +31 -2
  28. package/dist/io/textures.js +66 -18
  29. package/dist/io/unreal.js +22 -5
  30. package/dist/mcp/docs.d.ts +9 -2
  31. package/dist/mcp/docs.js +126 -2
  32. package/dist/mcp/inputs.d.ts +28 -0
  33. package/dist/mcp/inputs.js +377 -0
  34. package/dist/mcp/server.js +4 -1
  35. package/dist/mcp/tools.d.ts +1 -1
  36. package/dist/mcp/tools.js +118 -51
  37. package/dist/nodes/code.js +34 -6
  38. package/dist/nodes/copy.js +14 -4
  39. package/dist/nodes/flow.js +136 -35
  40. package/dist/nodes/heightfield.js +67 -11
  41. package/dist/nodes/texture.d.ts +1 -1
  42. package/dist/nodes/texture.js +61 -5
  43. package/dist/render/options.js +2 -1
  44. package/dist/render/raster.d.ts +2 -0
  45. package/dist/render/raster.js +3 -3
  46. package/dist/render/texview.d.ts +9 -1
  47. package/dist/render/texview.js +43 -2
  48. package/dist/viewer/app/api.js +64 -0
  49. package/dist/viewer/app/camera.js +250 -0
  50. package/dist/viewer/app/export-popover.js +68 -0
  51. package/dist/viewer/app/grid.js +217 -0
  52. package/dist/viewer/app/guides.js +402 -0
  53. package/dist/viewer/app/image-view.js +114 -0
  54. package/dist/viewer/app/keymap.js +199 -0
  55. package/dist/viewer/app/layers.js +244 -0
  56. package/dist/viewer/app/main.js +646 -0
  57. package/dist/viewer/app/markers.js +206 -0
  58. package/dist/viewer/app/mesh.js +172 -0
  59. package/dist/viewer/app/outliner.js +225 -0
  60. package/dist/viewer/app/outputs.css +73 -0
  61. package/dist/viewer/app/outputs.js +240 -0
  62. package/dist/viewer/app/props.js +432 -0
  63. package/dist/viewer/app/scene.js +529 -0
  64. package/dist/viewer/app/select.js +153 -0
  65. package/dist/viewer/app/selection.js +79 -0
  66. package/dist/viewer/app/status.js +20 -0
  67. package/dist/viewer/app/style.css +173 -0
  68. package/dist/viewer/app/undo.js +49 -0
  69. package/dist/viewer/app/units.js +51 -0
  70. package/dist/viewer/app/vars.js +64 -0
  71. package/dist/viewer/index.html +67 -184
  72. package/dist/viewer/outputs.d.ts +54 -0
  73. package/dist/viewer/outputs.js +436 -0
  74. package/dist/viewer/server.d.ts +47 -1
  75. package/dist/viewer/server.js +480 -17
  76. package/dist/workspace.d.ts +60 -4
  77. package/dist/workspace.js +131 -5
  78. package/package.json +9 -3
  79. package/shelf/brick_wall.json +1 -1
  80. package/shelf/wall_segment.json +2 -2
@@ -7,8 +7,37 @@ import type { PngImage } from './png-decode.ts';
7
7
  * per channel). deflate compresses (Node: zlib.deflateSync); without it the PNG uses stored (uncompressed) blocks.
8
8
  */
9
9
  export declare function imagePNG(img: Image, layer?: string, deflate?: (d: Uint8Array) => Uint8Array): Uint8Array;
10
- /** An image volume from a decoded PNG: grey → 1 channel, grey+alpha / RGB / RGBA → RGBA; samples / maxValue. */
11
- export declare function imageFromPNG(png: PngImage, name: string): Image;
10
+ /** How a PNG's channels become layers: see ImageFromPNGOptions. */
11
+ export declare const PNG_LAYERS: readonly ['auto', 'grey', 'rgba'];
12
+ export type PngLayers = (typeof PNG_LAYERS)[number];
13
+ export interface ImageFromPNGOptions {
14
+ /**
15
+ * 'auto' (default): grey files (1 or 2 channels) give a grey layer C, colour files an RGBA one; 'grey': a grey C
16
+ * (Rec. 709 luminance of a colour file); 'rgba': an RGBA C (grey g as g, g, g). A grey C puts the file's alpha, when
17
+ * it has one, in the grey layer "A" (Houdini's alpha plane).
18
+ */
19
+ layers?: PngLayers;
20
+ /**
21
+ * true (default): the samples are display (sRGB-encoded) values, which colour layers hold too, so they are kept as
22
+ * they are; false: they are linear light, and grey and RGB are encoded to display values (alpha never is).
23
+ */
24
+ srgb?: boolean;
25
+ }
26
+ /** An image volume from a decoded PNG: values = samples / maxValue (16-bit files keep their precision). */
27
+ export declare function imageFromPNG(png: PngImage, name: string, o?: ImageFromPNGOptions): Image;
28
+ /** The image name a file gives: its name without folders and extension ("inputs/brick.png" → "brick"). */
29
+ export declare function imageNameOf(path: string): string;
30
+ /**
31
+ * An image volume from the bytes of the PNG file `path` (the image_file node and the file node's .png path), named
32
+ * `name` or after the file (imageNameOf). Errors name the file; images over MAX_PIXELS are refused before decoding.
33
+ */
34
+ export declare function imageFromPNGFile(bytes: Uint8Array, path: string, o?: ImageFromPNGOptions & {
35
+ name?: string;
36
+ }): Image;
37
+ /** imageFromPNGFile on a file read through `readFile` (usually Host.readFile). */
38
+ export declare function readImageFile(readFile: (path: string) => Promise<Uint8Array>, path: string, o?: ImageFromPNGOptions & {
39
+ name?: string;
40
+ }): Promise<Image>;
12
41
  /** A file-name-safe version of an image name ("" → "image"). */
13
42
  export declare function safeFileName(name: string): string;
14
43
  /** The images a material table references ("image:<name>"), with the slots using each. */
@@ -1,7 +1,8 @@
1
- import { Image, channelsOf, imageRef, imageToBytes, opaque, referenceableImages } from '../core/image.js';
1
+ import { Image, LUMA, MAX_PIXELS, channelsOf, imageRef, imageToBytes, opaque, referenceableImages } from '../core/image.js';
2
2
  import { TEXTURE_SLOTS, mergeMaterialTables } from '../core/materials.js';
3
3
  import { encodePNG } from '../render/png.js';
4
4
  import { encodeGreyPNG, zlibStored } from './heightmap.js';
5
+ import { decodePNG } from './png-decode.js';
5
6
  import { baseOf, dirOf, joinPath } from './common.js';
6
7
  /**
7
8
  * PNG bytes of an image layer ("" = the main layer): 8-bit grey for 1-channel layers, RGBA for 4-channel ones (8 bits
@@ -13,32 +14,79 @@ export function imagePNG(img, layer = '', deflate) {
13
14
  return encodeGreyPNG(bytes, img.width, img.height, 8, deflate);
14
15
  return encodePNG(bytes, img.width, img.height, deflate ?? zlibStored);
15
16
  }
16
- /** An image volume from a decoded PNG: grey → 1 channel, grey+alpha / RGB / RGBA → RGBA; samples / maxValue. */
17
- export function imageFromPNG(png, name) {
18
- const n = png.width * png.height, ch = png.channels, d = png.data, k = 1 / (png.maxValue || 255);
17
+ /** How a PNG's channels become layers: see ImageFromPNGOptions. */
18
+ export const PNG_LAYERS = ['auto', 'grey', 'rgba'];
19
+ /** Display (sRGB-encoded) value of a linear one (io/gltf.ts converts back and forth for glTF's linear factors). */
20
+ const toDisplay = (c) => (c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055);
21
+ /** An image volume from a decoded PNG: values = samples / maxValue (16-bit files keep their precision). */
22
+ export function imageFromPNG(png, name, o = {}) {
23
+ const n = png.width * png.height, ch = png.channels, d = png.data, max = png.maxValue || 255, k = 1 / max;
24
+ // samples are integers 0..max: tables give every value once (alpha is never converted)
25
+ const lin = new Float32Array(max + 1);
26
+ for (let s = 0; s <= max; s++)
27
+ lin[s] = s * k;
28
+ const col = o.srgb === false ? Float32Array.from(lin, (_, s) => toDisplay(s * k)) : lin;
19
29
  const img = new Image(png.width, png.height, name);
20
- if (ch === 1) {
21
- const a = new Float32Array(n);
22
- for (let i = 0; i < n; i++)
23
- a[i] = d[i] * k;
24
- return img.set('C', a);
30
+ const mode = o.layers ?? 'auto';
31
+ if (mode === 'grey' || (mode === 'auto' && ch <= 2)) {
32
+ const C = new Float32Array(n);
33
+ if (ch <= 2)
34
+ for (let i = 0; i < n; i++)
35
+ C[i] = col[d[i * ch]];
36
+ else
37
+ for (let i = 0; i < n; i++) {
38
+ const p = i * ch;
39
+ C[i] = col[d[p]] * LUMA[0] + col[d[p + 1]] * LUMA[1] + col[d[p + 2]] * LUMA[2];
40
+ }
41
+ img.set('C', C);
42
+ if (ch === 2 || ch === 4) {
43
+ const A = new Float32Array(n);
44
+ for (let i = 0; i < n; i++)
45
+ A[i] = lin[d[i * ch + ch - 1]];
46
+ img.set('A', A);
47
+ }
48
+ return img;
25
49
  }
26
50
  const a = new Float32Array(n * 4);
27
51
  for (let i = 0; i < n; i++) {
28
- const o = i * ch;
29
- if (ch === 2) {
30
- a[i * 4] = a[i * 4 + 1] = a[i * 4 + 2] = d[o] * k;
31
- a[i * 4 + 3] = d[o + 1] * k;
32
- }
52
+ const p = i * ch, q = i * 4;
53
+ if (ch <= 2)
54
+ a[q] = a[q + 1] = a[q + 2] = col[d[p]];
33
55
  else {
34
- a[i * 4] = d[o] * k;
35
- a[i * 4 + 1] = d[o + 1] * k;
36
- a[i * 4 + 2] = d[o + 2] * k;
37
- a[i * 4 + 3] = ch === 4 ? d[o + 3] * k : 1;
56
+ a[q] = col[d[p]];
57
+ a[q + 1] = col[d[p + 1]];
58
+ a[q + 2] = col[d[p + 2]];
38
59
  }
60
+ a[q + 3] = ch === 2 || ch === 4 ? lin[d[p + ch - 1]] : 1;
39
61
  }
40
62
  return img.set('C', a);
41
63
  }
64
+ /** The image name a file gives: its name without folders and extension ("inputs/brick.png" → "brick"). */
65
+ export function imageNameOf(path) {
66
+ let p = String(path);
67
+ if (/^\w+:\/\//.test(p))
68
+ p = p.split(/[?#]/)[0];
69
+ return baseOf(p.replace(/[\\/]+$/, '')).replace(/\.[^.]*$/, '') || 'image';
70
+ }
71
+ /**
72
+ * An image volume from the bytes of the PNG file `path` (the image_file node and the file node's .png path), named
73
+ * `name` or after the file (imageNameOf). Errors name the file; images over MAX_PIXELS are refused before decoding.
74
+ */
75
+ export function imageFromPNGFile(bytes, path, o = {}) {
76
+ let png;
77
+ try {
78
+ png = decodePNG(bytes, MAX_PIXELS);
79
+ }
80
+ catch (e) {
81
+ throw new Error(`"${path}" cannot be read as a PNG image: ${e?.message ?? e}`);
82
+ }
83
+ return imageFromPNG(png, o.name || imageNameOf(path), o);
84
+ }
85
+ /** imageFromPNGFile on a file read through `readFile` (usually Host.readFile). */
86
+ export async function readImageFile(readFile, path, o = {}) {
87
+ const bytes = await readFile(path).catch((e) => { throw new Error(`cannot read "${path}": ${e?.message ?? e}`); });
88
+ return imageFromPNGFile(bytes, path, o);
89
+ }
42
90
  /** A file-name-safe version of an image name ("" → "image"). */
43
91
  export function safeFileName(name) {
44
92
  return String(name).replace(/[^A-Za-z0-9_.-]+/g, '_').replace(/^\.+/, '') || 'image';
package/dist/io/unreal.js CHANGED
@@ -4,12 +4,14 @@
4
4
  // component count, scale, location for the New Landscape panel and for the actor), the
5
5
  // files below and the manual import steps
6
6
  // heightmap.png / .json 16-bit heightmap and its sidecar (io/heightmap.ts; the manifest repeats its numbers)
7
- // layers/<layer>.png every other heightfield layer as an 8-bit weight map (0..1 layers as they are, other
7
+ // layers/<layer>.png every other heightfield layer (or only `layers` when given) as an 8-bit weight map (0..1 layers as they are, other
8
8
  // ranges normalised, the range recorded in the manifest)
9
9
  // objects.glb the geometry's meshes and packed instances (EXT_mesh_gpu_instancing), when it has prims
10
10
  // objects_expanded.glb the same with the instances as real geometry (for importers without the extension;
11
11
  // written up to EXPANDED_TRIANGLE_LIMIT triangles)
12
- // instances.csv the instance stream as Unreal PCG rows (one row per packed instance, Mesh column)
12
+ // objects_static.glb only the ordinary (non-instanced) meshes, at any instance count: what the Unreal plugin
13
+ // places as level geometry next to the instance stream (buildings, roads)
14
+ // instances.csv the instance stream as Unreal PCG rows (one row per packed instance, Mesh column)
13
15
  // instances.meshes.json Mesh → meshes/<Mesh>.glb, one GLB per packed source (metres, Y-up, at the origin)
14
16
  // tiles/<x>_<y>/ opt-in tile_size (quads): overlapping heightmaps, weight layers and instance CSVs;
15
17
  // version 2 adds tiles and tile_grid; root source files and shared meshes are retained
@@ -25,7 +27,7 @@ import { encodeGreyPNG, heightmapExport } from './heightmap.js';
25
27
  import { exportGLB } from './gltf.js';
26
28
  import { exportCSV, instanceMeshesJSON } from './points.js';
27
29
  import { safeName } from './common.js';
28
- import { expandedTriangles, flattenSource } from '../core/packed.js';
30
+ import { expandedTriangles, flattenSource, stripPacked } from '../core/packed.js';
29
31
  import { packedPrims, packedSourceNames } from './common.js';
30
32
  /** objects_expanded.glb is written while the expanded instances stay below this many triangles. */
31
33
  export const EXPANDED_TRIANGLE_LIMIT = 2_000_000;
@@ -100,9 +102,15 @@ export async function unrealLandscapeBundle(g, o = {}) {
100
102
  const layers = [];
101
103
  const usedNames = new Set();
102
104
  const weightBytes = new Map();
105
+ if (o.layers)
106
+ for (const want of o.layers)
107
+ if (!hf.layers.has(want))
108
+ throw new Error(`export unreal_landscape: layer "${want}" not found (layers: ${[...hf.layers.keys()].join(', ')})`);
103
109
  for (const [name, a] of hf.layers) {
104
110
  if (name === heightLayer)
105
111
  continue;
112
+ if (o.layers && !o.layers.includes(name))
113
+ continue; // --layers a,b: only these weight maps
106
114
  const st = layerStats(a);
107
115
  if (!st)
108
116
  continue;
@@ -127,8 +135,9 @@ export async function unrealLandscapeBundle(g, o = {}) {
127
135
  });
128
136
  }
129
137
  // meshes and instances of the same geometry
130
- let objects = null, expanded = null, instances = null;
131
- const glbOpts = { materials: o.materials, readFile: o.readFile, warn: o.warn, custom: o.custom };
138
+ let objects = null, expanded = null, statics = null, instances = null;
139
+ // Unreal's default glTF materials ignore vertex colours: the material colour must be the factor
140
+ const glbOpts = { materials: o.materials, readFile: o.readFile, warn: o.warn, custom: o.custom, materialColorWins: true };
132
141
  if (g.numPrims > 0) {
133
142
  files.push({ path: 'objects.glb', bytes: await exportGLB(g, glbOpts) });
134
143
  objects = 'objects.glb';
@@ -136,6 +145,13 @@ export async function unrealLandscapeBundle(g, o = {}) {
136
145
  files.push({ path: 'objects_expanded.glb', bytes: await exportGLB(g, { ...glbOpts, instancing: 'expand' }) });
137
146
  expanded = 'objects_expanded.glb';
138
147
  }
148
+ // the ordinary meshes alone (buildings, roads): small at any instance count, so an importer can place them as
149
+ // level geometry next to the instance stream even when objects_expanded.glb is skipped for size
150
+ const plain = stripPacked(g);
151
+ if (plain.numPrims > 0) {
152
+ files.push({ path: 'objects_static.glb', bytes: await exportGLB(plain, glbOpts) });
153
+ statics = 'objects_static.glb';
154
+ }
139
155
  }
140
156
  if (g.hasPacked()) {
141
157
  files.push({ path: 'instances.csv', bytes: new TextEncoder().encode(exportCSV(g, { pcg: true, scale: 100, warn: (m) => { if (!/other point/.test(m))
@@ -171,6 +187,7 @@ export async function unrealLandscapeBundle(g, o = {}) {
171
187
  layers,
172
188
  objects,
173
189
  objects_expanded: expanded,
190
+ objects_static: statics,
174
191
  instances,
175
192
  manual_import: LANDSCAPE_IMPORT_STEPS,
176
193
  };
@@ -1,5 +1,12 @@
1
- import type { NodeDef } from '../graph/types.ts';
2
- export declare const INSTRUCTIONS = "TopoTopo: a procedural geometry engine (Houdini-SOP style) you drive with JSON node graphs.\n\nWORKFLOW\n1. catalog \u2192 see node types (100+, Houdini names work as aliases: copytopoints, attribwrangle, polyextrude, mountain\u2026).\n2. help {nodes:[...]} for exact params before using unfamiliar nodes; help {topic:\"wrangle\"|\"graph\"|\"recipes\"\u2026} for concepts.\n3. build {graph} the WHOLE network in one call (dry_run:true validates without cooking). Every response reports errors per node with did-you-mean hints.\n4. render to SEE it (quad view: persp/front/right/top; backfaces are tinted blue = flipped normals; views \"quad_uv\" + checker:true for UVs; overlays for specific questions: wireframe \"hard\"/\"boundary\", views \"section\", glyphs \"N\", label_groups, shading \"matcap\", see help topic \"render\"). inspect for numbers (bbox, attributes, watertightness).\n5. edit {ops} for incremental changes (set params, insert nodes, rewire) \u2014 only downstream nodes re-cook.\n6. export to glb/gltf (materials, textures, meshopt), geo (Houdini), csv (Unreal PCG points / instance rows), points.json, usda, obj/ply/stl/json for Unreal, Blender, Houdini; unreal_landscape = a whole terrain scene for Unreal.\n7. verify: assert nodes guard invariants in the graph (watertight, counts, bbox, expr); explain {graph} reads a result in words; diff {a, b} shows what an edit changed. See help topic \"verify\".\nINSTANCING: copy_to_points {pack:true} / pack make packed instances (millions stay cheap; glb, usda and geo export them as instances, csv/points.json as one row per instance). See help topic \"instancing\".\nPCG: mask \u2192 scatter {density_mask} \u2192 orient \u2192 prune \u2192 copy_to_points {pack, variants, variant_weights}; points_along_curve, cluster, attribute_from_map. See help topic \"pcg\".\nCURVES: bezier / nurbs make spline prims (CVs + order, exact everywhere); convert_curve, resample, fillet_curve, offset_curve_3d, project_curve (roads on terrain), spline_fit, curve_attribs (curveu, frames, ramps). See help topic \"curves\".\nCLOTH: drape {steps} simulates a mesh as cloth over a collider (input 1) and the floor, from pins (pin_group / input 2) and in wind; inflate {pressure} puffs a closed mesh into a cushion or balloon. See help topic \"cloth\".\n\nCONVENTIONS: Y-up, metres, angles in degrees (wrangle trig uses radians). Houdini attribute names (P N Cd uv pscale orient up scale width name class piece). Polygons are closed prims, polylines are open prims.\nGRAPH: {\"nodes\": {\"name\": {\"type\": \"...\", \"in\": [\"upstream\", ...], \"params\": {...}}}, \"output\": \"name\", \"vars\": {\"k\": 1}}. Params accept \"=expr\" using vars (e.g. \"=height*0.5\"), time (frame, time) and other nodes (npoints('pts'), bbox('base').size.y, ch('box1/size')).\nTIME: build/edit/render/inspect/export take \"frame\" (default 1). solver = per-frame feedback (growth, accumulation). See help topic \"time\".\nPARTICLES: particles(emitter points, collision mesh, field) = Houdini POPs in one node: emission, gravity/drag/wind, curl noise, fields, floor and mesh collisions, trails as polylines; steps with the frame. See help topic \"particles\".\nESCAPE HATCHES: wrangle (per-element JS with @P/@Cd syntax), script (whole-geometry JS with the engine library), sdf_expr (signed distance functions). If a node is missing, write it with these.\nTERRAIN: heightfield \u2192 heightfield_noise \u2192 heightfield_erode \u2192 masks / heightfield_path \u2192 heightfield_scatter + copy_to_points, heightfield_to_mesh; export png16 for an Unreal Landscape, or unreal_landscape for the landscape + layers + instances bundle. See help topic \"heightfield\".\nPHYSICS: rbd_settle drops pieces (packed instances) onto a terrain/container/ground until they rest, so scattered rocks and props sit in contact; rbd_stack drops them one at a time into piles. See help topic \"rbd\".";
1
+ import type { GraphSpec, NodeDef } from '../graph/types.ts';
2
+ export declare const INSTRUCTIONS = "TopoTopo: a procedural geometry engine (Houdini-SOP style) you drive with JSON node graphs.\n\nWORKFLOW\n0. inputs {action:\"list\"} \u2192 the person's own files in the workspace inputs/ folder (guide curves, sites, zones, blockouts, PNG masks). Building a landscape or scene: look there first and build on them (help topic \"inputs\").\n1. catalog \u2192 see node types (100+, Houdini names work as aliases: copytopoints, attribwrangle, polyextrude, mountain\u2026).\n2. help {nodes:[...]} for exact params before using unfamiliar nodes; help {topic:\"wrangle\"|\"graph\"|\"recipes\"\u2026} for concepts.\n3. build {graph} the WHOLE network in one call (dry_run:true validates without cooking). Every response reports errors per node with did-you-mean hints.\n4. render to SEE it (quad view: persp/front/right/top; backfaces are tinted blue = flipped normals; views \"quad_uv\" + checker:true for UVs; overlays for specific questions: wireframe \"hard\"/\"boundary\", views \"section\", glyphs \"N\", label_groups, shading \"matcap\", see help topic \"render\"). inspect for numbers (bbox, attributes, watertightness).\n5. edit {ops} for incremental changes (set params, insert nodes, rewire) \u2014 only downstream nodes re-cook.\n6. export to glb/gltf (materials, textures, meshopt), geo (Houdini), csv (Unreal PCG points / instance rows), points.json, usda, obj/ply/stl/json for Unreal, Blender, Houdini; unreal_landscape = a whole terrain scene for Unreal.\n7. verify: assert nodes guard invariants in the graph (watertight, counts, bbox, expr); explain {graph} reads a result in words; diff {a, b} shows what an edit changed. See help topic \"verify\".\nOUTPUTS: publish intended deliverables in graph.outputs: {\"rotor\":{\"node\":\"rotor_final\",\"label\":\"Rotor\"}}. The default graph.output stays unchanged. The viewer lists every graph and named result, its generated textures and image layers; clicking a result isolates it. Rendered images and exports are saved and visible too. Use descriptive names; keep intermediate work in the node outliner. Rename/remove edits maintain published references.\nINSTANCING: copy_to_points {pack:true} / pack make packed instances (millions stay cheap; glb, usda and geo export them as instances, csv/points.json as one row per instance). See help topic \"instancing\".\nPCG: mask \u2192 scatter {density_mask} \u2192 orient \u2192 prune \u2192 copy_to_points {pack, variants, variant_weights}; points_along_curve, cluster, attribute_from_map. See help topic \"pcg\".\nCURVES: bezier / nurbs make spline prims (CVs + order, exact everywhere); convert_curve, resample, fillet_curve, offset_curve_3d, project_curve (roads on terrain), spline_fit, curve_attribs (curveu, frames, ramps). See help topic \"curves\".\nCLOTH: drape {steps} simulates a mesh as cloth over a collider (input 1) and the floor, from pins (pin_group / input 2) and in wind; inflate {pressure} puffs a closed mesh into a cushion or balloon. See help topic \"cloth\".\n\nCONVENTIONS: Y-up, metres, angles in degrees (wrangle trig uses radians). Houdini attribute names (P N Cd uv pscale orient up scale width name class piece). Polygons are closed prims, polylines are open prims.\nGRAPH: {\"nodes\": {\"name\": {\"type\": \"...\", \"in\": [\"upstream\", ...], \"params\": {...}}}, \"output\": \"name\", \"vars\": {\"k\": 1}}. Params accept \"=expr\" using vars (e.g. \"=height*0.5\"), time (frame, time) and other nodes (npoints('pts'), bbox('base').size.y, ch('box1/size')).\nHUMANS TUNE VARS: declare \"params\" for the 3\u20138 vars a person would tune ({\"k\": {type, desc, min, max, options, label, step}}); the live viewer (preview) shows them as sliders and menus. People change vars at any time, so read the graph (graphs {action:\"get\"}) before assuming a var's value. See help topic \"graph\".\nTIME: build/edit/render/inspect/export take \"frame\" (default 1). solver = per-frame feedback (growth, accumulation). See help topic \"time\".\nPARTICLES: particles(emitter points, collision mesh, field) = Houdini POPs in one node: emission, gravity/drag/wind, curl noise, fields, floor and mesh collisions, trails as polylines; steps with the frame. See help topic \"particles\".\nESCAPE HATCHES: wrangle (per-element JS with @P/@Cd syntax), script (whole-geometry JS with the engine library), sdf_expr (signed distance functions). If a node is missing, write it with these.\nTERRAIN: heightfield \u2192 heightfield_noise \u2192 heightfield_erode \u2192 masks / heightfield_path \u2192 heightfield_scatter + copy_to_points, heightfield_to_mesh; export png16 for an Unreal Landscape, or unreal_landscape for the landscape + layers + instances bundle. See help topic \"heightfield\".\nPHYSICS: rbd_settle drops pieces (packed instances) onto a terrain/container/ground until they rest, so scattered rocks and props sit in contact; rbd_stack drops them one at a time into piles. See help topic \"rbd\".";
3
+ /**
4
+ * The complete example of help topic "inputs": guides.glb's path.* curves cut roads into a heightfield and its zone.*
5
+ * areas mask a tree scatter; the vars a person tunes are exposed as params. test/mcp.ts cooks it on a real guides.glb.
6
+ */
7
+ export declare const INPUTS_EXAMPLE: GraphSpec;
8
+ /** A graph as JSON with one node (and one param) per line: readable in a topic page, and JSON.parse gives it back. */
9
+ export declare function graphLines(g: GraphSpec): string;
3
10
  export declare const TOPICS: Record<string, string>;
4
11
  export declare function topicList(): string[];
5
12
  export declare function nodeDoc(def: NodeDef): string;
package/dist/mcp/docs.js CHANGED
@@ -2,6 +2,7 @@ import { CATEGORY_ORDER } from '../graph/registry.js';
2
2
  export const INSTRUCTIONS = `TopoTopo: a procedural geometry engine (Houdini-SOP style) you drive with JSON node graphs.
3
3
 
4
4
  WORKFLOW
5
+ 0. inputs {action:"list"} → the person's own files in the workspace inputs/ folder (guide curves, sites, zones, blockouts, PNG masks). Building a landscape or scene: look there first and build on them (help topic "inputs").
5
6
  1. catalog → see node types (100+, Houdini names work as aliases: copytopoints, attribwrangle, polyextrude, mountain…).
6
7
  2. help {nodes:[...]} for exact params before using unfamiliar nodes; help {topic:"wrangle"|"graph"|"recipes"…} for concepts.
7
8
  3. build {graph} the WHOLE network in one call (dry_run:true validates without cooking). Every response reports errors per node with did-you-mean hints.
@@ -9,6 +10,7 @@ WORKFLOW
9
10
  5. edit {ops} for incremental changes (set params, insert nodes, rewire) — only downstream nodes re-cook.
10
11
  6. export to glb/gltf (materials, textures, meshopt), geo (Houdini), csv (Unreal PCG points / instance rows), points.json, usda, obj/ply/stl/json for Unreal, Blender, Houdini; unreal_landscape = a whole terrain scene for Unreal.
11
12
  7. verify: assert nodes guard invariants in the graph (watertight, counts, bbox, expr); explain {graph} reads a result in words; diff {a, b} shows what an edit changed. See help topic "verify".
13
+ OUTPUTS: publish intended deliverables in graph.outputs: {"rotor":{"node":"rotor_final","label":"Rotor"}}. The default graph.output stays unchanged. The viewer lists every graph and named result, its generated textures and image layers; clicking a result isolates it. Rendered images and exports are saved and visible too. Use descriptive names; keep intermediate work in the node outliner. Rename/remove edits maintain published references.
12
14
  INSTANCING: copy_to_points {pack:true} / pack make packed instances (millions stay cheap; glb, usda and geo export them as instances, csv/points.json as one row per instance). See help topic "instancing".
13
15
  PCG: mask → scatter {density_mask} → orient → prune → copy_to_points {pack, variants, variant_weights}; points_along_curve, cluster, attribute_from_map. See help topic "pcg".
14
16
  CURVES: bezier / nurbs make spline prims (CVs + order, exact everywhere); convert_curve, resample, fillet_curve, offset_curve_3d, project_curve (roads on terrain), spline_fit, curve_attribs (curveu, frames, ramps). See help topic "curves".
@@ -16,31 +18,144 @@ CLOTH: drape {steps} simulates a mesh as cloth over a collider (input 1) and the
16
18
 
17
19
  CONVENTIONS: Y-up, metres, angles in degrees (wrangle trig uses radians). Houdini attribute names (P N Cd uv pscale orient up scale width name class piece). Polygons are closed prims, polylines are open prims.
18
20
  GRAPH: {"nodes": {"name": {"type": "...", "in": ["upstream", ...], "params": {...}}}, "output": "name", "vars": {"k": 1}}. Params accept "=expr" using vars (e.g. "=height*0.5"), time (frame, time) and other nodes (npoints('pts'), bbox('base').size.y, ch('box1/size')).
21
+ HUMANS TUNE VARS: declare "params" for the 3–8 vars a person would tune ({"k": {type, desc, min, max, options, label, step}}); the live viewer (preview) shows them as sliders and menus. People change vars at any time, so read the graph (graphs {action:"get"}) before assuming a var's value. See help topic "graph".
19
22
  TIME: build/edit/render/inspect/export take "frame" (default 1). solver = per-frame feedback (growth, accumulation). See help topic "time".
20
23
  PARTICLES: particles(emitter points, collision mesh, field) = Houdini POPs in one node: emission, gravity/drag/wind, curl noise, fields, floor and mesh collisions, trails as polylines; steps with the frame. See help topic "particles".
21
24
  ESCAPE HATCHES: wrangle (per-element JS with @P/@Cd syntax), script (whole-geometry JS with the engine library), sdf_expr (signed distance functions). If a node is missing, write it with these.
22
25
  TERRAIN: heightfield → heightfield_noise → heightfield_erode → masks / heightfield_path → heightfield_scatter + copy_to_points, heightfield_to_mesh; export png16 for an Unreal Landscape, or unreal_landscape for the landscape + layers + instances bundle. See help topic "heightfield".
23
26
  PHYSICS: rbd_settle drops pieces (packed instances) onto a terrain/container/ground until they rest, so scattered rocks and props sit in contact; rbd_stack drops them one at a time into piles. See help topic "rbd".`;
27
+ /**
28
+ * The complete example of help topic "inputs": guides.glb's path.* curves cut roads into a heightfield and its zone.*
29
+ * areas mask a tree scatter; the vars a person tunes are exposed as params. test/mcp.ts cooks it on a real guides.glb.
30
+ */
31
+ export const INPUTS_EXAMPLE = {
32
+ nodes: {
33
+ guides: { type: 'file', params: { path: 'inputs/guides.glb' } },
34
+ roles: { type: 'wrangle', in: ['guides'], params: { run_over: 'prims', code: "s@role = s@role || @name.split('.')[0].toLowerCase();" } },
35
+ paths: { type: 'delete', in: ['roles'], params: { group: '@role=path', keep: true } },
36
+ zones: { type: 'delete', in: ['roles'], params: { group: '@role=zone', keep: true } },
37
+ areas: { type: 'ends', in: ['zones'], params: { mode: 'close' } },
38
+ ground: { type: 'heightfield', params: { size: '=extent', resolution: 1 } },
39
+ hills: { type: 'heightfield_noise', in: ['ground'], params: { amplitude: '=relief', element_size: 60 } },
40
+ road: { type: 'heightfield_path', in: ['hills', 'paths'], params: { width: '=road_width' } },
41
+ zone_mask: { type: 'heightfield_mask_by_object', in: ['road', 'areas'], params: { by: 'footprint', distance: 0, falloff: 3 } },
42
+ off_road: { type: 'heightfield_layer', in: ['zone_mask'], params: { layer: 'mask', expression: 'mask * (1 - path)' } },
43
+ spots: { type: 'heightfield_scatter', in: ['off_road'], params: { density: '=tree_density', align: 'upright', seed: 3 } },
44
+ tree: { type: 'tree_lowpoly', params: { height: 7 } },
45
+ trees: { type: 'copy_to_points', in: ['tree', 'spots'], params: { pack: true } },
46
+ land: { type: 'heightfield_to_mesh', in: ['off_road'], params: { color: 'terrain' } },
47
+ out: { type: 'merge', in: ['land', 'trees'] },
48
+ },
49
+ output: 'out',
50
+ vars: { extent: 200, relief: 6, road_width: 5, tree_density: 0.02 },
51
+ params: {
52
+ relief: { type: 'float', min: 0, max: 30, step: 0.5, label: 'Hill height (m)', desc: 'terrain relief in metres' },
53
+ road_width: { type: 'float', min: 2, max: 12, label: 'Road width (m)', desc: 'flat width of the roads drawn as path.*' },
54
+ tree_density: { type: 'float', min: 0, max: 0.1, step: 0.005, label: 'Trees per m²', desc: 'tree density inside the zone.* areas' },
55
+ },
56
+ };
57
+ /** A graph as JSON with one node (and one param) per line: readable in a topic page, and JSON.parse gives it back. */
58
+ export function graphLines(g) {
59
+ const rows = (o) => Object.entries(o).map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)}`).join(',\n');
60
+ const parts = Object.entries(g).map(([k, v]) => (k === 'nodes' || k === 'params') && v && typeof v === 'object' ? `${JSON.stringify(k)}: {\n${rows(v)}\n }` : `${JSON.stringify(k)}: ${JSON.stringify(v)}`);
61
+ return `{${parts.join(',\n ')}}`;
62
+ }
24
63
  export const TOPICS = {
25
64
  graph: `GRAPH FORMAT
26
65
  {
27
66
  "nodes": {
28
67
  "ground": {"type": "grid", "params": {"size": [10, 10], "divisions": [20, 20]}},
29
- "pts": {"type": "scatter", "in": ["ground"], "params": {"count": 200, "seed": 1}},
68
+ "pts": {"type": "scatter", "in": ["ground"], "params": {"count": "=rock_count", "seed": 1}},
30
69
  "rock": {"type": "sphere", "params": {"type": "ico", "level": 2, "radius": 0.2}},
31
70
  "rocks": {"type": "copy_to_points", "in": ["rock", "pts"]},
32
71
  "out": {"type": "merge", "in": ["ground", "rocks"]}
33
72
  },
34
73
  "output": "out",
35
- "vars": {"density": 200}
74
+ "vars": {"rock_count": 200},
75
+ "params": {"rock_count": {"type": "int", "min": 0, "max": 2000, "label": "Rocks", "desc": "number of rocks"}}
36
76
  }
77
+ - "vars": graph constants that "=expr" params read. "params": one entry per var a person would tune (3–8 of them):
78
+ {type (float int bool string enum), desc, min, max, options (enum), label (control name), step (slider step)}.
79
+ The live viewer (preview) renders them as controls: sliders for numbers with a range, menus for options, checkboxes
80
+ for bools. They are also the parameters when the graph is saved as an asset.
81
+ - People change vars at any time (the viewer's controls edit the stored graph): read it with graphs {action:"get"}
82
+ before assuming a var's value, and edit {op:"vars"} rather than rebuilding to change one. When a person asks to
83
+ keep a value, keep it.
37
84
  - "in": ordered input list (null = unconnected), a single name, or {"inputName": node} (names from help).
38
85
  - Output node: "output", else a node named out/OUT, else the last unconnected node.
86
+ - Optional "outputs": {"rotor":{"node":"rotor_final","label":"Rotor"}} publishes additional deliverables beside
87
+ the default. Publish complete assets and useful separate parts; the viewer isolates each on click and lists
88
+ its textures/layers. Rename/remove edits maintain references. The latest render of each graph node is saved as
89
+ exports/renders/<graph>-<node>.png (each render overwrites it) unless save_to is supplied; explicit render/export
90
+ destinations are registered in the viewer too.
91
+ - Optional "export" (on the graph for the default output, or on an entry of "outputs") is where that output
92
+ exports: a format ("glb" → exports/<graph or output id>.glb) or {"format":"obj","path":"parts/a.obj","up_axis":"z"}
93
+ (the export tool's options), always inside exports/. The person's Export buttons in the viewer, export {output} /
94
+ export {all:true} and the CLI's --export-all write these targets; set one with edit {op:"export", output?, export}.
39
95
  - Node names: letters, digits, _ . - Node types & params are case/underscore-insensitive and accept Houdini-style aliases.
40
96
  - "bypass": true passes input 0 through. "note": free text. "ui": {x,y} reserved for the future GUI.
41
97
  - Vectors: a single number broadcasts ("size": 2 → [2,2,2]). Colours accept [r,g,b], "#ff8800" or names.
42
98
  - build mode "merge" upserts nodes into the existing graph; edit ops: set, add, remove (auto-rewires), connect, rename, output, vars, bypass, replace, insert (after a node, rewiring its consumers).
43
99
  - Several graphs can coexist (name param); graph_ref composes them. Graphs persist as JSON in the workspace.`,
100
+ inputs: `HUMAN INPUT (guides, blockouts and masks in the workspace inputs/ folder)
101
+ People feed in their own material by dropping files into <workspace>/inputs/ (or onto the live viewer, or with its Add
102
+ input button): meshes, guide curves and points drawn in Blender, Houdini or Unreal, placeholder blockouts, PNG masks and
103
+ heightmaps. Every tool answer ends with a note when inputs/ has files new or changed since your last call. Before building a
104
+ landscape or scene, call inputs {action:"list"}; inputs {action:"describe", path} tells what one holds (polygons, open
105
+ polylines, loose points, packed prims; bbox in metres; attributes; every name and role value; materials). Read a file
106
+ with a file node, the path relative to the workspace: {"type":"file","params":{"path":"inputs/guides.glb"}}.
107
+ MARKERS: what a person places by clicking in the live viewer (Site / Path / Zone / Volume modes) is written as
108
+ inputs/markers.json with the same name and role attributes (sites: points with pscale and orient; paths open and
109
+ zones closed polylines; volumes closed boxes), so describe it and read it like any other input.
110
+ FORMATS: glb/gltf (+Y up already; object names → prim "name", materials → "material"), obj/stl/ply (no up axis:
111
+ Unreal exports and Blender exports with Up = Z are Z-up, so add "up_axis":"z"), geo/bgeo (Houdini), csv and
112
+ points.json (one point per row; columns such as name, role, pscale become point attributes), blend (needs Blender
113
+ installed; it converts the file headlessly). Images: png (8/16-bit grey or colour).
114
+ CONVENTIONS: only Houdini attributes and groups, all optional, no add-on to install.
115
+ name string: the object name (the glTF importer writes it on prims; csv/points.json name columns land on points)
116
+ role string on points or prims: what a guide means: site, path, zone, volume, blockout (below)
117
+ People mark roles with a name prefix on their objects: site.well, path.river, zone.park, volume.no_trees,
118
+ blockout.house (Blender's .001 suffixes do no harm), or with a custom property "role" on the object (it arrives as an
119
+ attribute once the importer keeps glTF extras).
120
+ Derive role from the prefix, keeping a role the file brought (run_over "points" for point guides):
121
+ {"type":"wrangle","in":["guides"],"params":{"run_over":"prims","code":"s@role = s@role || @name.split('.')[0].toLowerCase();"}}
122
+ Select with group patterns: "@role=path", or straight by name prefix "@name=path.*" (a glob; space-separated tokens
123
+ are a union: "@name=path.* @name=zone.*"), e.g. {"type":"delete","in":["guides"],"params":{"group":"@name=path.*","keep":true}}.
124
+ ROLES AND RECIPES
125
+ site a point marks a location (a well, a lone tree, a house spot); pscale = radius in metres, orient or N = heading.
126
+ copy_to_points(asset, sites) {pack:true} puts a copy on every site (scaled by pscale, turned by orient/N);
127
+ foreach {mode:"points"} builds one per site ("=attr.pscale", "=attr.name" in the body). A site drawn as a
128
+ small mesh: pack {by:"name", pivot:"bbox_center_bottom"} gives one anchor point per object.
129
+ path an open polyline: road, river, wall, fence line. project_curve(paths, terrain mesh) {spacing:1} drapes it on
130
+ a mesh; heightfield_path(terrain, paths) {width, shoulder} cuts a road into a heightfield (layer "path");
131
+ points_along_curve {spacing, side_offset, both_sides} for posts, lamps or trees along it; sweep
132
+ {shape:"ribbon", radius} for a road surface or a wall; heightfield_mask_by_object {by:"distance"} for a river bed.
133
+ zone a closed polyline or a filled polygon is an area: a park, a field, a no-build zone. Loops drawn as edges
134
+ arrive as polylines ending on their first point: ends {mode:"close"} makes them polygons.
135
+ On a heightfield, heightfield_mask_by_object(terrain, zones) {by:"footprint", falloff} rasterises the polygons
136
+ into the mask layer (mode add / max / subtract / multiply combines several): heightfield_scatter reads it, and
137
+ heightfield_noise, heightfield_remap, heightfield_blur, heightfield_terrace and heightfield_layer blend their
138
+ effect by it (mask_layer:"mask").
139
+ Points inside a zone (top view), as point group in_zone:
140
+ {"type":"wrangle","in":["pts","zones"],"params":{"run_over":"points","code":"@group_in_zone = intersect(1, [@P.x, 1e6, @P.z], [0, -1, 0]).prim >= 0 ? 1 : 0;"}}
141
+ then delete {group:"in_zone"} or {group:"in_zone", keep:true}.
142
+ volume a closed mesh or box: a region where scatter thins out or thickens. prune(pts, volumes) {mode:"near_geometry",
143
+ distance:0} removes the points inside; mask(ground mesh, volumes) {by:"inside", falloff} → scatter
144
+ {density_mask:"mask"} scatters there (invert:true for a hole); group {class:"point", pattern:"@mask>0.5"} groups
145
+ them. Let volumes reach below the ground. On a heightfield use heightfield_mask_by_object {by:"footprint"}.
146
+ blockout a placeholder mesh to replace: bound {per_piece:true} gives each piece's box; foreach {mode:"pieces",
147
+ piece_attrib:"name"} runs a body per object, where a shelf asset takes its size from the piece
148
+ ("width": "=bbox('piece').size.x", piece = the body's input node) and match_size(asset, piece) moves it in
149
+ place. A footprint polygon feeds the house asset directly (its input 0).
150
+ IMAGES: PNG masks and heightmaps. heightfield_from_image {path:"inputs/height.png", size:[x, z], height_min, height_max}
151
+ makes a terrain (16-bit greys keep the detail); with a heightfield connected it writes into layer (e.g. "mask",
152
+ height_min 0, height_max 1). attribute_from_map {path, projection:"xz", size, center, name:"mask"} samples it onto
153
+ points (scatter density_mask, prune {mode:"density"}). Textures: image_file {path:"inputs/albedo.png"} (or a file
154
+ node) loads it as an image named "albedo" for the texture nodes and texture_to_material {base_color:"albedo"}.
155
+ EXAMPLE: inputs/guides.glb holds path.* curves and zone.* areas: roads cut into hills, trees in the zones off the road,
156
+ the vars a person would tune exposed as params:
157
+ ${graphLines(INPUTS_EXAMPLE)}
158
+ Afterwards render it, and tell the person which of their guides you used for what.`,
44
159
  expressions: `PARAM EXPRESSIONS
45
160
  Any non-code param can be a string starting with "=": a JS expression evaluated with graph vars.
46
161
  "vars": {"floors": 6, "h": 3} → "height": "=floors*h", "count": "=floors*4"
@@ -243,6 +358,9 @@ WORKFLOW
243
358
  → heightfield_erode {iterations: 60} (gullies and sediment fans; writes flow, sediment, debris, water)
244
359
  → heightfield_path (road along a curve, writes "path") · heightfield_project (stamp meshes/curves: plateaus, ridges)
245
360
  → heightfield_mask_by_feature (slope/height/curvature/direction/flow/occlusion) → heightfield_scatter → copy_to_points
361
+ (heightfield_scatter {align: "upright"} for trees and buildings, {align: "slope"} for rocks that follow the ground;
362
+ without align, copy_to_points aims each copy's +Z along N, which lays Y-up models down;
363
+ {min_distance: 12} for lots that never overlap; heightfield_project {level: "mean"} levels the pads under them)
246
364
  → heightfield_to_mesh {color: "terrain"} for polygons (layers become point attributes), or export png16.
247
365
  Other sources: heightfield_from_mesh (project polygons along Y), heightfield_from_image (8/16-bit PNG).
248
366
  Layer tools: heightfield_layer (expression over layers "height * mask + 2", or copy/rename/delete/fill), remap, blur,
@@ -300,6 +418,10 @@ GENERATORS (no input, resolution [w, h], name): tex_noise (perlin/simplex/value/
300
418
  hex, dots, grid; gap = mortar, bevel, variation; layer "id" = random per cell) · tex_cells (Voronoi distance / edges /
301
419
  id / crackle) · tex_gradient · tex_shape (disc, square, polygon, star, ring; falloff; tile) · tex_splatter (blobs) ·
302
420
  tex_scratches (strokes) · tex_text.
421
+ FILES: image_file {path:"inputs/brick.png"} loads a PNG (8/16-bit, grey or colour; the file node reads .png the same way)
422
+ as an image named after the file ("brick"; name sets another); layers auto/grey/rgba; srgb (default on) keeps the stored
423
+ values, which suits colour and data maps alike (off only for linear-light colour). Input 0 = a geometry to add it to,
424
+ so image_file nodes chain. People drop their own textures and masks into inputs/.
303
425
  FILTERS (input 0 = an image; image/layer pick what to process, the result replaces that layer; name renames; every
304
426
  other image and the mesh pass through): tex_blend (normal, add, subtract, multiply, screen, overlay, min, max,
305
427
  difference, divide; opacity; input 2 = mask) · tex_levels · tex_curves (ramp) · tex_blur (gaussian/box, tileable
@@ -594,6 +716,8 @@ Examples: render {views:["persp","section"], section_axis:"z", wireframe:"hard",
594
716
  CLI: --wireframe all --section y:0.5 --clip x:0.5 --glyphs N --labels groups,pieces --shading matcap --legend --size 1600 --scale 2
595
717
  --no-view-labels`,
596
718
  export: `EXPORT
719
+ export {output:"crate"} / export {} (the default output) / export {all:true}: the graph's export targets (help topic
720
+ "graph": "export" on the graph or an output), written inside exports/, the same files the viewer's Export buttons write.
597
721
  export {path:"exports/thing.glb"} — format from the extension:
598
722
  glb / gltf glTF 2.0 via gltf-transform (recommended for Unreal, Blender, web)
599
723
  geo Houdini JSON geometry: every attribute (all classes), group and polyline, lossless (precision 64 or 32)
@@ -0,0 +1,28 @@
1
+ import type { Workspace, InputFile } from '../workspace.ts';
2
+ import type { ToolDef } from './tools.ts';
3
+ /** name prefixes that mark a guide's role (help topic "inputs") */
4
+ export declare const ROLES: readonly ['site', 'path', 'zone', 'volume', 'blockout'];
5
+ interface Described {
6
+ /** one line for inputs list */
7
+ short: string;
8
+ /** the full inputs describe text (without the header line) */
9
+ long: string;
10
+ failed: boolean;
11
+ }
12
+ /** inputs/ file → its description, keyed by path and invalidated by the host's file stamp (mtime + size). */
13
+ export declare class InputDescriber {
14
+ ws: Workspace;
15
+ private cache;
16
+ constructor(ws: Workspace);
17
+ /** Describe one file (cached while its stamp holds). */
18
+ describe(f: InputFile): Promise<Described>;
19
+ /** Whether describe() would answer from the cache. */
20
+ cached(f: InputFile): boolean;
21
+ /** Forget files that are gone. */
22
+ prune(keep: InputFile[]): void;
23
+ /** Cook the one-node graph a file node would be (never a workspace graph) and read the result. */
24
+ private geometry;
25
+ private image;
26
+ }
27
+ export declare function inputsTool(ws: Workspace): ToolDef;
28
+ export {};