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/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\".\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.
@@ -16,24 +17,69 @@ CLOTH: drape {steps} simulates a mesh as cloth over a collider (input 1) and the
16
17
 
17
18
  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
19
  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')).
20
+ 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
21
  TIME: build/edit/render/inspect/export take "frame" (default 1). solver = per-frame feedback (growth, accumulation). See help topic "time".
20
22
  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
23
  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
24
  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
25
  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".`;
26
+ /**
27
+ * The complete example of help topic "inputs": guides.glb's path.* curves cut roads into a heightfield and its zone.*
28
+ * areas mask a tree scatter; the vars a person tunes are exposed as params. test/mcp.ts cooks it on a real guides.glb.
29
+ */
30
+ export const INPUTS_EXAMPLE = {
31
+ nodes: {
32
+ guides: { type: 'file', params: { path: 'inputs/guides.glb' } },
33
+ roles: { type: 'wrangle', in: ['guides'], params: { run_over: 'prims', code: "s@role = s@role || @name.split('.')[0].toLowerCase();" } },
34
+ paths: { type: 'delete', in: ['roles'], params: { group: '@role=path', keep: true } },
35
+ zones: { type: 'delete', in: ['roles'], params: { group: '@role=zone', keep: true } },
36
+ areas: { type: 'ends', in: ['zones'], params: { mode: 'close' } },
37
+ ground: { type: 'heightfield', params: { size: '=extent', resolution: 1 } },
38
+ hills: { type: 'heightfield_noise', in: ['ground'], params: { amplitude: '=relief', element_size: 60 } },
39
+ road: { type: 'heightfield_path', in: ['hills', 'paths'], params: { width: '=road_width' } },
40
+ zone_mask: { type: 'heightfield_mask_by_object', in: ['road', 'areas'], params: { by: 'footprint', distance: 0, falloff: 3 } },
41
+ off_road: { type: 'heightfield_layer', in: ['zone_mask'], params: { layer: 'mask', expression: 'mask * (1 - path)' } },
42
+ spots: { type: 'heightfield_scatter', in: ['off_road'], params: { density: '=tree_density', align: 'upright', seed: 3 } },
43
+ tree: { type: 'tree_lowpoly', params: { height: 7 } },
44
+ trees: { type: 'copy_to_points', in: ['tree', 'spots'], params: { pack: true } },
45
+ land: { type: 'heightfield_to_mesh', in: ['off_road'], params: { color: 'terrain' } },
46
+ out: { type: 'merge', in: ['land', 'trees'] },
47
+ },
48
+ output: 'out',
49
+ vars: { extent: 200, relief: 6, road_width: 5, tree_density: 0.02 },
50
+ params: {
51
+ relief: { type: 'float', min: 0, max: 30, step: 0.5, label: 'Hill height (m)', desc: 'terrain relief in metres' },
52
+ road_width: { type: 'float', min: 2, max: 12, label: 'Road width (m)', desc: 'flat width of the roads drawn as path.*' },
53
+ tree_density: { type: 'float', min: 0, max: 0.1, step: 0.005, label: 'Trees per m²', desc: 'tree density inside the zone.* areas' },
54
+ },
55
+ };
56
+ /** A graph as JSON with one node (and one param) per line: readable in a topic page, and JSON.parse gives it back. */
57
+ export function graphLines(g) {
58
+ const rows = (o) => Object.entries(o).map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)}`).join(',\n');
59
+ 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)}`);
60
+ return `{${parts.join(',\n ')}}`;
61
+ }
24
62
  export const TOPICS = {
25
63
  graph: `GRAPH FORMAT
26
64
  {
27
65
  "nodes": {
28
66
  "ground": {"type": "grid", "params": {"size": [10, 10], "divisions": [20, 20]}},
29
- "pts": {"type": "scatter", "in": ["ground"], "params": {"count": 200, "seed": 1}},
67
+ "pts": {"type": "scatter", "in": ["ground"], "params": {"count": "=rock_count", "seed": 1}},
30
68
  "rock": {"type": "sphere", "params": {"type": "ico", "level": 2, "radius": 0.2}},
31
69
  "rocks": {"type": "copy_to_points", "in": ["rock", "pts"]},
32
70
  "out": {"type": "merge", "in": ["ground", "rocks"]}
33
71
  },
34
72
  "output": "out",
35
- "vars": {"density": 200}
73
+ "vars": {"rock_count": 200},
74
+ "params": {"rock_count": {"type": "int", "min": 0, "max": 2000, "label": "Rocks", "desc": "number of rocks"}}
36
75
  }
76
+ - "vars": graph constants that "=expr" params read. "params": one entry per var a person would tune (3–8 of them):
77
+ {type (float int bool string enum), desc, min, max, options (enum), label (control name), step (slider step)}.
78
+ The live viewer (preview) renders them as controls: sliders for numbers with a range, menus for options, checkboxes
79
+ for bools. They are also the parameters when the graph is saved as an asset.
80
+ - People change vars at any time (the viewer's controls edit the stored graph): read it with graphs {action:"get"}
81
+ before assuming a var's value, and edit {op:"vars"} rather than rebuilding to change one. When a person asks to
82
+ keep a value, keep it.
37
83
  - "in": ordered input list (null = unconnected), a single name, or {"inputName": node} (names from help).
38
84
  - Output node: "output", else a node named out/OUT, else the last unconnected node.
39
85
  - Node names: letters, digits, _ . - Node types & params are case/underscore-insensitive and accept Houdini-style aliases.
@@ -41,6 +87,64 @@ export const TOPICS = {
41
87
  - Vectors: a single number broadcasts ("size": 2 → [2,2,2]). Colours accept [r,g,b], "#ff8800" or names.
42
88
  - 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
89
  - Several graphs can coexist (name param); graph_ref composes them. Graphs persist as JSON in the workspace.`,
90
+ inputs: `HUMAN INPUT (guides, blockouts and masks in the workspace inputs/ folder)
91
+ People feed in their own material by dropping files into <workspace>/inputs/ (or onto the live viewer): meshes, guide
92
+ curves and points drawn in Blender, Houdini or Unreal, placeholder blockouts, PNG masks and heightmaps. Before building a
93
+ landscape or scene, call inputs {action:"list"}; inputs {action:"describe", path} tells what one holds (polygons, open
94
+ polylines, loose points, packed prims; bbox in metres; attributes; every name and role value; materials). Read a file
95
+ with a file node, the path relative to the workspace: {"type":"file","params":{"path":"inputs/guides.glb"}}.
96
+ MARKERS: what a person places by clicking in the live viewer (Site / Path / Zone / Volume modes) is written as
97
+ inputs/markers.json with the same name and role attributes (sites: points with pscale and orient; paths open and
98
+ zones closed polylines; volumes closed boxes), so describe it and read it like any other input.
99
+ FORMATS: glb/gltf (+Y up already; object names → prim "name", materials → "material"), obj/stl/ply (no up axis:
100
+ Unreal exports and Blender exports with Up = Z are Z-up, so add "up_axis":"z"), geo/bgeo (Houdini), csv and
101
+ points.json (one point per row; columns such as name, role, pscale become point attributes), blend (needs Blender
102
+ installed; it converts the file headlessly). Images: png (8/16-bit grey or colour).
103
+ CONVENTIONS: only Houdini attributes and groups, all optional, no add-on to install.
104
+ name string: the object name (the glTF importer writes it on prims; csv/points.json name columns land on points)
105
+ role string on points or prims: what a guide means: site, path, zone, volume, blockout (below)
106
+ People mark roles with a name prefix on their objects: site.well, path.river, zone.park, volume.no_trees,
107
+ blockout.house (Blender's .001 suffixes do no harm), or with a custom property "role" on the object (it arrives as an
108
+ attribute once the importer keeps glTF extras).
109
+ Derive role from the prefix, keeping a role the file brought (run_over "points" for point guides):
110
+ {"type":"wrangle","in":["guides"],"params":{"run_over":"prims","code":"s@role = s@role || @name.split('.')[0].toLowerCase();"}}
111
+ Select with group patterns: "@role=path", or straight by name prefix "@name=path.*" (a glob; space-separated tokens
112
+ are a union: "@name=path.* @name=zone.*"), e.g. {"type":"delete","in":["guides"],"params":{"group":"@name=path.*","keep":true}}.
113
+ ROLES AND RECIPES
114
+ site a point marks a location (a well, a lone tree, a house spot); pscale = radius in metres, orient or N = heading.
115
+ copy_to_points(asset, sites) {pack:true} puts a copy on every site (scaled by pscale, turned by orient/N);
116
+ foreach {mode:"points"} builds one per site ("=attr.pscale", "=attr.name" in the body). A site drawn as a
117
+ small mesh: pack {by:"name", pivot:"bbox_center_bottom"} gives one anchor point per object.
118
+ path an open polyline: road, river, wall, fence line. project_curve(paths, terrain mesh) {spacing:1} drapes it on
119
+ a mesh; heightfield_path(terrain, paths) {width, shoulder} cuts a road into a heightfield (layer "path");
120
+ points_along_curve {spacing, side_offset, both_sides} for posts, lamps or trees along it; sweep
121
+ {shape:"ribbon", radius} for a road surface or a wall; heightfield_mask_by_object {by:"distance"} for a river bed.
122
+ zone a closed polyline or a filled polygon is an area: a park, a field, a no-build zone. Loops drawn as edges
123
+ arrive as polylines ending on their first point: ends {mode:"close"} makes them polygons.
124
+ On a heightfield, heightfield_mask_by_object(terrain, zones) {by:"footprint", falloff} rasterises the polygons
125
+ into the mask layer (mode add / max / subtract / multiply combines several): heightfield_scatter reads it, and
126
+ heightfield_noise, heightfield_remap, heightfield_blur, heightfield_terrace and heightfield_layer blend their
127
+ effect by it (mask_layer:"mask").
128
+ Points inside a zone (top view), as point group in_zone:
129
+ {"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;"}}
130
+ then delete {group:"in_zone"} or {group:"in_zone", keep:true}.
131
+ volume a closed mesh or box: a region where scatter thins out or thickens. prune(pts, volumes) {mode:"near_geometry",
132
+ distance:0} removes the points inside; mask(ground mesh, volumes) {by:"inside", falloff} → scatter
133
+ {density_mask:"mask"} scatters there (invert:true for a hole); group {class:"point", pattern:"@mask>0.5"} groups
134
+ them. Let volumes reach below the ground. On a heightfield use heightfield_mask_by_object {by:"footprint"}.
135
+ blockout a placeholder mesh to replace: bound {per_piece:true} gives each piece's box; foreach {mode:"pieces",
136
+ piece_attrib:"name"} runs a body per object, where a shelf asset takes its size from the piece
137
+ ("width": "=bbox('piece').size.x", piece = the body's input node) and match_size(asset, piece) moves it in
138
+ place. A footprint polygon feeds the house asset directly (its input 0).
139
+ IMAGES: PNG masks and heightmaps. heightfield_from_image {path:"inputs/height.png", size:[x, z], height_min, height_max}
140
+ makes a terrain (16-bit greys keep the detail); with a heightfield connected it writes into layer (e.g. "mask",
141
+ height_min 0, height_max 1). attribute_from_map {path, projection:"xz", size, center, name:"mask"} samples it onto
142
+ points (scatter density_mask, prune {mode:"density"}). Textures: image_file {path:"inputs/albedo.png"} (or a file
143
+ node) loads it as an image named "albedo" for the texture nodes and texture_to_material {base_color:"albedo"}.
144
+ EXAMPLE: inputs/guides.glb holds path.* curves and zone.* areas: roads cut into hills, trees in the zones off the road,
145
+ the vars a person would tune exposed as params:
146
+ ${graphLines(INPUTS_EXAMPLE)}
147
+ Afterwards render it, and tell the person which of their guides you used for what.`,
44
148
  expressions: `PARAM EXPRESSIONS
45
149
  Any non-code param can be a string starting with "=": a JS expression evaluated with graph vars.
46
150
  "vars": {"floors": 6, "h": 3} → "height": "=floors*h", "count": "=floors*4"
@@ -243,6 +347,9 @@ WORKFLOW
243
347
  → heightfield_erode {iterations: 60} (gullies and sediment fans; writes flow, sediment, debris, water)
244
348
  → heightfield_path (road along a curve, writes "path") · heightfield_project (stamp meshes/curves: plateaus, ridges)
245
349
  → heightfield_mask_by_feature (slope/height/curvature/direction/flow/occlusion) → heightfield_scatter → copy_to_points
350
+ (heightfield_scatter {align: "upright"} for trees and buildings, {align: "slope"} for rocks that follow the ground;
351
+ without align, copy_to_points aims each copy's +Z along N, which lays Y-up models down;
352
+ {min_distance: 12} for lots that never overlap; heightfield_project {level: "mean"} levels the pads under them)
246
353
  → heightfield_to_mesh {color: "terrain"} for polygons (layers become point attributes), or export png16.
247
354
  Other sources: heightfield_from_mesh (project polygons along Y), heightfield_from_image (8/16-bit PNG).
248
355
  Layer tools: heightfield_layer (expression over layers "height * mask + 2", or copy/rename/delete/fill), remap, blur,
@@ -300,6 +407,10 @@ GENERATORS (no input, resolution [w, h], name): tex_noise (perlin/simplex/value/
300
407
  hex, dots, grid; gap = mortar, bevel, variation; layer "id" = random per cell) · tex_cells (Voronoi distance / edges /
301
408
  id / crackle) · tex_gradient · tex_shape (disc, square, polygon, star, ring; falloff; tile) · tex_splatter (blobs) ·
302
409
  tex_scratches (strokes) · tex_text.
410
+ FILES: image_file {path:"inputs/brick.png"} loads a PNG (8/16-bit, grey or colour; the file node reads .png the same way)
411
+ as an image named after the file ("brick"; name sets another); layers auto/grey/rgba; srgb (default on) keeps the stored
412
+ values, which suits colour and data maps alike (off only for linear-light colour). Input 0 = a geometry to add it to,
413
+ so image_file nodes chain. People drop their own textures and masks into inputs/.
303
414
  FILTERS (input 0 = an image; image/layer pick what to process, the result replaces that layer; name renames; every
304
415
  other image and the mesh pass through): tex_blend (normal, add, subtract, multiply, screen, overlay, min, max,
305
416
  difference, divide; opacity; input 2 = mask) · tex_levels · tex_curves (ramp) · tex_blur (gaussian/box, tileable
@@ -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 {};