{"id":46296,"plugin_id":"plugin_asdk_app_6acd235a70a48191812b3ba9c471b4ac","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-24T13:29:11.297Z","digest":"8802710566d54298830b526eb20725be58c507c2940adcda89a77636b3398a74","against":null,"payload":{"description":"Composing full scenes: units & spatial layout, grounding objects, scatter loops & cloner randomness for dense dressing, lighting & shadow tuning, fog/AO atmosphere, camera framing, color palettes, typography & the font catalog, prettify/style, arranging and grouping.","included_files":[],"name":"spline-3d-building-scenes","skill_md_contents":"---\nname: spline-3d-building-scenes\ndescription: \"Composing full scenes: units & spatial layout, grounding objects, scatter loops & cloner randomness for dense dressing, lighting & shadow tuning, fog/AO atmosphere, camera framing, color palettes, typography & the font catalog, prettify/style, arranging and grouping.\"\n---\n\n# Building Scenes\n\nGuidance for composing a complete, well-framed 3D scene from scratch. Honor\nthe style register first (see # Style register in your core guidance):\nunstated style = believable, not toy. Buildings — houses, towers, rooms —\nadditionally have their own `architecture` skill with proportion and\nconstruction recipes; load it alongside this one for those.\n\n## Units & default sizes\nThe editor uses arbitrary world units (loosely \"pixels\"). Keep a mental model\nof the defaults so spacing reads correctly:\n\n- A new mesh (`add('Mesh','CubeGeometry')` / `mesh('Cube', {})`) appears at the\n  current BUILD CENTER — the camera's look-at, or the free anchor the editor\n  picks when the scene already has content (given in `<editor-context>`) — NOT\n  necessarily the world origin. It arrives auto-selected. Pin only the axes you\n  must (usually `y`) and offset with `translate` so independent builds don't\n  pile up at the origin.\n- Default 2D-ish primitives (Rectangle) are about 320 x 320.\n- Default Sphere / Cube are about 100 x 100 x 100.\n- The default camera sits around z = 1000 looking at the origin, so objects in\n  the -300..300 range read at a comfortable size. Spread multiple objects by a\n  few hundred units, not a few units.\n- BELIEVABLE-register anchor: treat 1 real-world meter ≈ 50 units and size\n  real-world subjects from the # Real-world scale table in your core guidance\n  (person 88, door 105, storey 150, car 225 long, two-lane road ≈ 300 wide,\n  street tree 300-600). A real PLACE — a street, plaza, yard — spans\n  THOUSANDS of units: build it at true scale and pull the camera back rather\n  than shrinking the world to fit the default view; a shrunken place is what\n  reads as a toy diorama.\n\nBecause every newly created object is auto-selected, the cleanest pattern is:\ncreate -> immediately transform/color the selection -> create the next object.\n\n## A minimal repeatable recipe\n```\n// 1. one hero object on the ground\nmesh('Sphere', { width: 200, height: 200, depth: 200 });\ncolor('#e0552b');\nposition({ y: 100 }); // sit it on the floor; x/z stay at the build center\nrename('Hero');\n\n// 2. a ground plane (rotate the plane flat, push it down)\nmesh('Plane', { width: 3000, height: 3000 });\nrotation({ x: -90 });\nposition({ y: 0 }); // ground it; x/z stay at the build center\ncolor('#f2f0eb');\nrename('Floor');\n\n// 3. light + a soft background\nlight('Directional', { intensity: 0.9 });\ntranslate({ x: 400, y: 600, z: 500 }); // offset the key light from the build center\nbackground('#dfe3e8');\n```\n\n## Shape geometry parameters\nBeyond `width`/`height`/`depth`, each primitive takes shape-specific params. Set\nthem at creation (`mesh('Cube', { cornerRadius: 20 })`) or later on the selection\nwith `geometry({ ... })`. The useful ones per shape:\n\n- `Cube`: `cornerRadius` + `cornerSegments` (a rounded box reads as soft/plush —\n  a stylized cue; NOTE it SPAWNS at cornerRadius 8, so pass `cornerRadius: 0`\n  for crisp believable hard surfaces), `widthSegments`/`heightSegments`/\n  `depthSegments`.\n- `Sphere`: `widthSegments`, `heightSegments` — smoothness; low counts = faceted.\n- `Cylinder`: thickness = `radius` (= half `width`; default `width` is 100, so\n  an unsized cylinder is a fat 100-wide slug — pass `radius` or `width` for a\n  slim pole/trunk). `radiusTop`/`radiusBottom` set only the TAPER — they're\n  ratios relative to `width` (unequal → a taper/frustum), NOT absolute radii, so\n  equal values do not make it thinner. Plus `radialSegments`, `openEnded`,\n  `hollow` (0-1 wall thinness, NOT a boolean — 0.8 → a pipe/tube; `true`\n  degenerates to a zero-thickness wall), `cornerRadius` (spawns at 8 — set 0\n  for a crisp rim).\n- `Cone`: thickness = `radius` (= half `width`; same default-100 caveat as\n  `Cylinder`), `radialSegments`, `openEnded`, `cornerRadiusTop`,\n  `cornerRadiusBottom`.\n- `Pyramid`: `radialSegments` = base side count (3 = tetrahedron, 4 = square\n  base). A 4-sided Pyramid is AXIS-ALIGNED — its faces line up with a `Cube`'s\n  walls, so it caps a box squarely with the SAME Y rotation as the walls; never\n  add a 45° yaw to \"align\" a roof, that extra rotation is what twists it off.\n  (A 4-sided `Cone` sits diagonally by comparison — use `Pyramid` for a square\n  roof.) Also `cornerRadius`.\n- `Torus`: `radialSegments`, `tubularSegments`, `cornerRadius`.\n- `TorusKnot`: `tube` (thickness), `p` and `q` (the knot winding — try 2 and 3),\n  `radialSegments`, `tubularSegments`.\n- `Ellipse` (circle/disc): `innerRadius` — a PERCENT of the outer radius\n  (0-100, not world units; > 0 → a ring/annulus), `spikes`.\n- `Polygon`: `spikes` = number of sides (3 = triangle, 6 = hexagon),\n  `cornerRadius`.\n- `Star`: `spikes` = number of points, `innerRadiusPercent` (point sharpness),\n  `cornerRadius`.\n- `Plane`: `widthSegments`, `heightSegments` (subdivide for a displaced surface).\n- `Rectangle`/`Triangle`: `cornerRadius`.\n- `Dodecahedron`/`Icosahedron`: `detail` (0 = base solid; higher subdivides\n  toward a sphere), `corner`, `cornerSides`.\n- `Helix`: `revolutions`, `radius`, `pathRadius` (coil thickness), `segments`.\n- `Backdrop`: `cornerRadius` — an infinity-curve floor-to-wall sweep; a great\n  seamless studio background behind a hero object.\n- `Lathe`: a profile revolved around Y into a vase / bottle / bowl / glass.\n  `points` is ONE cubic bezier — exactly 4 `[x, y]` control points (x =\n  radius >= 0, y-down), not an arbitrary polyline; `segments` and\n  `verticalSegments` control smoothness. Load the `lathe` skill for the full\n  contract and profile recipes before building one.\n- `Decal`: a projector box that stamps its OWN material onto every mesh\n  surface inside it — the tool for stickers, labels, logos, racing numbers,\n  splashes, impacts, graffiti and wear (see the SURFACE DETAIL rule in your\n  core guidance). `width`/`height` size the stamp, `depth` is the reach —\n  keep it generous, the box must pass THROUGH the receiving surface —\n  and `offset` lifts the skin off the surface (default 1, so it never\n  z-fights). Aim it like the flat Rectangle it replaces: unrotated stamps a\n  camera-facing side, `rotation({ x: -90 })` a floor/tabletop, `{ y: 90 }`\n  a +X side panel, `{ y: 180 }` the far side. Image + alpha-cutout recipe:\n  the `materials-and-look` skill's Decals section.\n- `Text`: `text` (the string; `\\n` breaks a line), `fontSize`, `font` (the\n  font id — see ## Typography below), `lineHeight` and `letterSpacing`\n  (multipliers of the font size), `horizontalAlign` / `verticalAlign`. Width\n  and height are the text BOX (`mesh('Text', { fontSize })` sizes one for you);\n  a box narrower than the copy wraps it.\n\nFlat shapes (`Rectangle`, `Ellipse`, `Polygon`, `Star`, `Triangle`, `Text`) also\ntake a `depth` that EXTRUDES the 2D outline into a solid, plus `extrudeBevelSize`\n/ `extrudeBevelSegments` to round the extruded edge — e.g. a chunky 3D star or a\nbeveled logo.\n\nPartial-sweep params are plain DEGREES: a torus `arc` (0-360), a sphere\n`thetaLength` (0-180, sweeping down from the top pole), a cone/cylinder\n`thetaLength` (0-360), a backdrop/ellipse `angle`. `mesh('Torus', { arc: 180 })`\nis half a donut; `{ thetaLength: 90 }` on a sphere is a dome.\n\n## Typography\nA `Text` left on the default Roboto reads as unstyled. Pick a face that\ncarries the register: geometric sans (`Poppins`, `Outfit`, `Space Grotesk`)\nfor product/tech, a condensed display cut (`Bebas Neue`, `Anton`, `Oswald`)\nfor posters and signage, a serif (`Playfair Display`, `Fraunces`, `Lora`) for\neditorial/luxury, mono (`JetBrains Mono`, `Space Mono`) for code and HUDs,\nscript (`Caveat`, `Pacifico`) for handwritten notes. Two families per scene is\nplenty — one for headlines, one for body — and keep it consistent with the\nscene's HTML overlay if there is one.\n\nValid font ids (format `{Family}_{variant}`, default `Roboto_regular`):\n\n- **Sans (UI)**: `Inter` (regular, 500, 600, 700), `Roboto` (regular, 500, 700, italic), `Poppins` (regular, 500, 600, 700), `Work Sans` (regular, 500, 600, 700), `DM Sans` (regular, 500, 700), `Manrope` (regular, 500, 600, 700), `Lato` (regular, 700, italic), `Open Sans` (regular, 500, 600, 700), `Nunito` (regular, 600, 700), `Montserrat` (regular, 500, 600, 700), `Source Sans 3` (regular, 600, 700), `Space Grotesk` (regular, 500, 700), `Outfit` (regular, 500, 700), `Figtree` (regular, 500, 700), `Plus Jakarta Sans` (regular, 500, 700), `IBM Plex Sans` (regular, 500, 700), `PT Sans` (regular, 700)\n- **Display**: `Bebas Neue` (regular), `Oswald` (regular, 500, 700), `Anton` (regular), `Archivo Black` (regular)\n- **Serif**: `Fraunces` (regular, 600, 700), `Playfair Display` (regular, 600, 700, italic), `Lora` (regular, 600, 700, italic), `Merriweather` (regular, 700), `EB Garamond` (regular, 600, italic), `Cormorant Garamond` (regular, 600, italic), `Source Serif 4` (regular, 600, 700), `Libre Baskerville` (regular, 700, italic), `Roboto Slab` (regular, 500, 700)\n- **Monospace**: `JetBrains Mono` (regular, 500, 700), `Fira Code` (regular, 500, 700), `IBM Plex Mono` (regular, 500, 700), `Space Mono` (regular, 700), `Roboto Mono` (regular, 500, 700)\n- **Handwritten / Script**: `Caveat` (regular, 700), `Pacifico` (regular), `Dancing Script` (regular, 700), `Permanent Marker` (regular), `Shadows Into Light` (regular)\n\nJoin family and variant with `_` — `mesh('Text', { text: 'Studio', font: 'Playfair Display_700' })`, `geometry({ font: 'JetBrains Mono_regular' })`, `Roboto_italic`. Spaces inside family names are preserved.\n\n- **Weight is the variant, not a separate prop** — there is no `bold`/`fontWeight` on text. Bold = the `_700` id.\n- **Italic exists only where listed** — `Roboto`, `Lato`, `Playfair Display`, `Lora`, `EB Garamond`, `Cormorant Garamond`, `Libre Baskerville`. Asking for it elsewhere silently gives you the upright cut.\n- A family in the catalog with a cut it doesn't list resolves to its nearest weight; a family that isn't in the catalog at all THROWS — don't invent fonts outside this list, and don't try to load a URL yourself.\n- Fonts load asynchronously: the glyphs appear a beat after `run_code` returns. That is normal — don't \"fix\" it by re-setting the font.\n- 3D extrusion (a `depth` on the text) reads best on the heavier cuts. Hairline and script faces lose their strokes once extruded and beveled.\n\n## Grounding\nObjects are created centered on their origin, so a 200-tall sphere whose center\nis at y=0 will be half-buried in a floor at y=0. Lift it by half its height\n(`position({ y: 100 })`). For a flat floor, create a `Plane` and rotate it\n`{ x: -90 }` so it lies in the XZ plane.\n\nSHADOW-CATCHER FLOORS: when the floor exists only to ground the subject —\nproduct shots, a logo or hero object over a clean background color/gradient,\nminimal studio looks — make it invisible and keep just the shadows:\n`visibility({ shadowCatcher: true })` on that Rectangle/Plane (only those two\ngeometries support it). The slab disappears, the page background shows\nthrough, and the cast shadows stay — instant \"floating over the page\" look\nwith real grounding. Pair it with a background color and a directional key\nlight so there IS a shadow to catch. Skip it when the floor's own surface is\npart of the scene (boards, roads, grass — those want a real material).\n\nTip: the all-in-one `studio()` / `stage('studio')` helper builds a neutral\nground + key light + studio materials in one call. Prefer it when the user just\nwants \"a nice render\" rather than a specific layout.\n\n## Lighting setup\n- LIGHT BUDGET — reuse before adding. Check the scene snapshot for existing\n  lights (DirectionalLight / PointLight / SpotLight) first; if one is there,\n  `select(o => o.isLight)` and `updateLight` / `position` it rather than\n  adding another. One directional key over the built-in ambient fill is\n  enough for most DAYLIGHT scenes — cap those at 3 lights unless the user\n  explicitly asks for more. NIGHT/INTERIOR scenes may run more local\n  Points/Spots when each has a visible job (a lamp, a sign, a doorway);\n  keep shadows on at most 1–2 of them.\n- `light('Directional', { intensity: 0.9 })` is the workhorse key light. It\n  spawns offset +100 on each axis from the build center; `translate` it to\n  taste, e.g. further up and to one side.\n- LIGHT SIZE follows the scene footprint: a directional light's shadows\n  exist only inside its `size` footprint (default 2000 world units). Big\n  builds — buildings, city blocks, castles — outgrow that fast, and the\n  shadow field visibly cuts off mid-scene. Whenever the scene spans more\n  than ~2000 units, set `size: 4000` or more, matched to the whole area\n  (`updateLight({ size: 6000 })` on the key light).\n- OPTIONALLY add a dimmer `light('Point', { intensity: 0.4 })` on the opposite\n  side as fill to soften shadows — only when the key + ambient alone leave the\n  shadow side too harsh, not by default.\n- EXPOSURE BUDGET: every light stacks on top of the always-on ambient fill\n  (default intensity 0.75), so the sum of key + fill + ambient is the scene's\n  exposure — and the right numbers depend on the REGISTER. DAYLIGHT: key at\n  0.8–1, fills 0.3–0.4, and when you add a fill, dim the ambient to\n  compensate; if the live shot still reads flat after repositioning, the key\n  may go up to 2 — past that fix the ambient or palette instead.\n  NIGHT/INTERIOR: dim the ambient and let local Points/Spots CARRY the\n  exposure at `intensity` 2–8, shaped with `distance`/`decay` into tight\n  pools — judge by the live capture (subject readable, highlights not\n  clipping), and pair glowing materials with a small Point/Spot: emissive\n  glows but does not illuminate surroundings.\n- Light `color` accepts hex/CSS strings; a warm key + cool fill reads well.\n- Valid light types: `'Point'`, `'Spot'`, `'Directional'` (the alias appends\n  `Light` for you, so `'Directional'` -> `'DirectionalLight'`).\n- SHADOW TUNING is real and cheap: every light takes `shadows` (on/off,\n  default on) and `shadowRadius` (softness — 1 is crisp, raise toward 4–8\n  for soft). `light('Directional', { intensity: 0.9, shadowRadius: 4 })`.\n  `shadowResolution` (map size) STAYS at its low default (512) — the right\n  performance call for generated scenes; raise it (1024/2048) only when the\n  user explicitly asks for crisper/hero shadows.\n- The page has a built-in AMBIENT fill (hemisphere-style sky/ground light).\n  Tint and dim/boost it with `environment({ ambient: { color: '#bcd4ff',\n  intensity: 0.7 } })` — a cool ambient + warm key is the classic outdoor mix.\n  Don't fake ambience with extra point lights.\n\n## Atmosphere (fog, AO, soft shadows)\nFlat single-color backgrounds with no depth cues read as tech demos. The page\nenvironment fixes that in one or two calls:\n- `fog({ useBackgroundColor: true, near: 800, far: 4000 })` — distance fog\n  blending toward the background color; distant objects melt into the sky.\n  Tune `near`/`far` to your scene's depth (they're world units from the\n  camera). An explicit `color` overrides the background blend.\n- `environment({ ao: true })` — ambient occlusion: contact/crevice darkening\n  that visually GROUNDS objects on surfaces. Cheap realism, almost always\n  worth enabling for built scenes.\n- LEAVE the global shadow type (`environment({ softShadow })`) alone: it is\n  'normal' (stock filtering) in new documents — the right performance call for\n  generated scenes, since PCSS soft shadows cost render time every frame — and\n  soft in older ones, whose look was authored against it. Shape the shadow\n  look with the per-light `shadowRadius` / `shadowResolution` instead.\n  Switch to soft shadows ('low' | 'medium' | 'high' quality) only when the\n  user explicitly asks for soft shadows.\n- `fog('none')` / `environment({ ao: false })` switch them off.\n\n## Camera framing\n- `lookFrom(spec)` CHOOSES the vantage and aims the viewport camera. `spec` is a\n  preset ('front'|'back'|'left'|'right'|'top'|'bottom'|'iso'|'threeQuarter') or\n  `{ azimuth?, elevation?, distance?, target? }` in degrees (azimuth 0=front,\n  90=right, -90=left, 180=back; elevation 0=eye level, positive=above looking\n  down, 90=top-down). This is how you get a hero 3/4, a top-down flat-lay, or a\n  straight-on shot. Omitted `distance` FITS the target's real bounds (the\n  selection's with `target: 'selection'`, the scene's content cluster\n  otherwise — a bare preset call also aims at that cluster): the framing is\n  computed, so never hand-pick distance numbers for final shots. Don't chain\n  `focus()` after it in the same call — `lookFrom` already frames.\n- `focus(target?)` / `centerView(target?)` re-frames the selection (or a passed\n  target) at the CURRENT angle, without rotating; `zoom(target)` is an alias of\n  focus for objects.\n- `viewport('isometric'|'perspective'|'orthographic'|'reset')` changes the\n  PROJECTION only — angle and projection are independent.\n- `activateCamera(cameraRef?)` makes a scene camera (from `add('PerspectiveCamera')`)\n  the live editor view; `activateCamera('personal')` returns to the default camera.\n- These aliases move the USER's viewport — use them to set the final\n  presentation angle, e.g. `lookFrom('threeQuarter')` in a NEW run_code call\n  once the build is done. For YOUR OWN verification screenshots use the\n  `set_view` TOOL instead: `set_view({ objects: [...] })` fits your private\n  screenshot camera to your work without moving the user; nudge its\n  azimuth/elevation a few degrees to add depth or reveal a hidden side, then\n  `take_screenshot()`.\n- SET-DRESSING RECIPES (the SUPPORTING CAST rule's concrete forms — pick\n  the matching register, build scatter via loops/cloners with varied\n  rotation/scale):\n  tabletop/food — tray or board, napkin/cloth, side dish, drink, garnish\n  or crumb scatter, one utensil;\n  shop/interior — signage (text primitive), shelf or counter clutter, a\n  floor mat/rug, 2–3 hanging or wall fixtures, a door/window hint;\n  outdoor/nature — ground break-up (rock + grass-tuft scatter along paths\n  and edges), 2–3 background silhouettes (trees, hills), sky treatment;\n  arcade/game — marquee text, coin/ticket scatter, a neighboring cabinet\n  or prop that hints at the venue;\n  workshop/studio — tools on the wall, work-in-progress clutter, one\n  visible light fixture as an object.\n- FINISHING RITUAL (every scene build ends with this, in order): (0) the\n  BRIEF-NOUN CHECKLIST from the quality bar passes — every concrete noun\n  in the prompt exists, at its implied count, and is identifiable in the\n  frame; (1) the\n  scene has a deliberate background — `background(...)` or `style([...])`,\n  never the editor's default grid void; (2) set the user's final vantage\n  with `lookFrom`/`focus` on the hero content — for an INTERIOR brief that\n  vantage is INSIDE the room at eye level (`select` the focal furniture,\n  `lookFrom({ ..., target: 'selection' })`; never an exterior dollhouse\n  shot of the shell); (3) verify with\n  `take_screenshot({ view: 'live', hero: [<the brief's main object\n  names>] })` — the USER'S actual view, not your\n  private camera, which can frame the work perfectly while the user's view\n  shows none of it — and the `hero` names are REQUIRED on the final\n  check: they make the check measure the SUBJECT itself; (4) the live\n  capture returns an EXPOSURE CHECK, a\n  FRAMING CHECK, and a HERO CHECK — treat any warning as a failed final:\n  if the frame crops\n  the subject, shows it tiny, shows bare background/grid/sky, reads\n  near-black, washed out, or as one flat color, or reports the hero\n  off-frame, cropped, hidden behind other geometry, under ~18% of the\n  frame, or overflowing it (too close), fix it and re-check. SUBJECT\n  SCALE: a single-object brief (a dish, a toy, one machine, one small\n  creature) is a CLOSE-UP — the subject at a third to half the frame\n  height, not a speck in a room-wide shot. Do this\n  EARLY too (as soon as the main structures exist), so a run cut off at a\n  step ceiling still leaves a composed view. (5) Passing the check is the\n  FLOOR: now polish — name the biggest weakness in the live frame (staging\n  density, composition, lighting, materials, a missing brief noun), fix it,\n  re-verify, and COMPARE with the previous capture: keep the change only if\n  the frame reads better, REVERSE it if worse (fog/haze and camera\n  pull-backs are the usual regressions), and keep the subject filling the\n  frame after every cycle. At most your Refine allowance of cycles (TWO by\n  default; a raised allowance arrives as a \"REFINE setting\" line in\n  <editor-context>), then stop and summarize — once the measured checks\n  pass, further nits are churn; keep cycling past the allowance ONLY while\n  a declared time budget remains (IMPROVE WHILE BUDGET REMAINS in the\n  quality bar).\n\n## Color palettes\nPick 3-5 cohesive hex colors and apply them per object with `color()`. Match\nthe palette to the style register: an unstated style (the believable default)\nwants muted, desaturated real-world tones with value contrast; save saturated\ncandy primaries for explicitly cute/stylized asks. Two powerful global helpers\nexist:\n\n- `prettify()` — auto-harmonizes every material's colors into a pleasing scheme\n  (no arguments). Great as a finishing pass.\n- `style(['#hex', ...], seed?)` — recolors all materials AND the background from\n  a palette you supply. Pass an array of hex strings.\n\n```\nstyle(['#1b1f3b', '#e94f37', '#f6f7eb', '#44bba4']);\n```\n\n## Arranging multiple objects\n- `arrange(margins?, target?)` spreads the selection apart so nothing overlaps,\n  leaving `margins` units of gap. Select first, then call `arrange(40)`.\n- For regular grids/rows, prefer the `clone` cloner (see below) or place items\n  explicitly with `position()`.\n- `align(path, { animate })` snaps the selection onto a path object.\n\n### Scattering N varied objects with a loop (the density workhorse)\nBounded `for`/`while` loops around alias calls run fine in the sandbox — a\nloop of create + transform calls is the NORMAL way to place many objects.\nVary size, position, and rotation per iteration so nothing repeats exactly:\n```\nfor (let i = 0; i < 10; i++) {\n  const s = 0.5 + rand();\n  mesh('Icosahedron', { width: 60 * s, detail: 0 });\n  color(i % 2 ? '#8d8d93' : '#75757c');\n  position({ y: 30 * s });\n  translate({ x: (rand() - 0.5) * 1200, z: (rand() - 0.5) * 1200 });\n  rotation({ y: rand() * 360, z: (rand() - 0.5) * 30 });\n}\n```\nSparse scenes read as unfinished — \"a scene\" implies secondary and tertiary\ndressing, and loops make it cheap. Keep counts bounded (tens, not thousands).\n\nWhen the scattered copies share one look (the same rock grey, one foliage\ngreen), don't restyle each iteration — create the material ASSET first and\nlink every copy to it (see materials-and-look). One `updateMaterial` then\nrestyles the whole batch:\n```\ncreateMaterial('Rock Grey', { layers: [{ type: 'color', color: '#8d8d93' }] });\nfor (let i = 0; i < 10; i++) {\n  mesh('Icosahedron', { width: 60, detail: 0 });   // new mesh is the selection\n  applyMaterial('Rock Grey');\n  // …position/rotation variation as above\n}\n```\n\n### Cloners (procedural duplication)\n`clone({...})` turns the selected object into a cloner. Each `type` has a\nsame-named sub-object of settings; unspecified keys keep sensible defaults.\nCalling `clone` on an object that is ALREADY a cloner MERGES — partial props\nedit it in place (`get_objects` returns the current settings in this same\nshape). `hideBase: true` hides the original object. Useful shapes:\n```\n// a 5x1x5 grid; grid.size is the spacing per axis (default: object size +10%)\nclone({ type: 'grid', count: [5, 1, 5], grid: { size: [120, 0, 120] } });\n\n// linear accumulates PER STEP — each clone offsets/rotates (in degrees) by\n// this much more than the previous one: stacks, spiral staircases, twisted\n// towers, fanned cards. scale is a LINEAR ramp, not compounding: clone i gets\n// 1 + (scale - 1) * i, so 0.94 hits zero around clone 17 — keep shrink\n// factors close to 1 on long stacks. (Default is a small +X offset.)\nclone({ type: 'linear', count: 12,\n  linear: { position: [0, 40, 0], rotation: [0, 25, 0], scale: [0.94, 0.94, 0.94] } });\n\n// a radial ring — SET radial.radius (the default, 2x the object's size, is\n// usually too tight). start/end (degrees) make a partial arc, alignment: true\n// faces each clone along the ring, axis ('x'|'y'|'z') picks the ring plane.\n// Unlike linear, radial's position/rotation/scale are a CONSTANT offset\n// applied to every clone — position [0, 40, 0] raises the whole ring, it\n// does NOT spiral.\nclone({ type: 'radial', count: 12, radial: { radius: 260, alignment: true } });\n```\n\nCloners can RANDOMIZE every instance — position/rotation/scale jitter with a\nseed — which turns a rigid grid into organic scatter in one call. Unspecified\n`randomnessObject` keys keep their defaults (`strength` 100, `movement` 1).\nThe scatter is STATIC: `movement` is a phase offset into the noise, not\nmotion — changing it (like `seed`) just reshuffles the pattern; nothing\ndrifts on its own. `freqScale`/`noiseType` ('perlin'|'simplex') shape the\nnoise:\n```\n// a 6x1x6 patch of the selected object, jittered into a natural-looking field\nclone({ type: 'grid', count: [6, 1, 6], randomness: true,\n  randomnessObject: { strength: 100, position: [80, 0, 80],\n    rotation: [0, 180, 0], scale: [0.4, 0.4, 0.4], seed: 3 } });\n```\n\n`cloneTo(target, { count, spreadType, seed })` distributes clones onto another\nobject's SURFACE — trees/rocks over a terrain mesh, spikes over a sphere.\n`select` returns an ARRAY of matches and `cloneTo` takes ONE object, so grab\nthe target with `[0]` first:\n```\nconst island = select(o => o.name === 'Island')[0];\nselect(o => o.name === 'Tree');\ncloneTo(island, { count: 18, spreadType: 'random', seed: 7, hideBase: true });\n```\n(`spreadType`: 'random' | 'polygon_center' | 'vertex' | 'edge' — for the\nnon-random types `count` means a PERCENTAGE (0-100) of the candidate spots\nand `seed` is ignored. `align: 'normal'` tilts each clone to the surface\nnormal; `align: 'axis'` + `axis` ('x'|'-x'|'y'|'-y'|'z'|'-z') keeps a fixed\norientation instead. `hideBase: true` hides the original left at its old\nspot.)\n\n## Procedural custom geometry (`customMesh`)\nThe parametric primitives can't make terrain, rocks, or organic forms — for those,\nGENERATE the geometry. Build flat `vertices` (and `indices`) arrays with loops +\nMath, displacing points to shape them, then pass them to `customMesh` (normals are\nauto-computed). Patterns:\n- Terrain/island: a grid or concentric rings of vertices on the XZ plane with `y`\n  displaced by `Math.sin`/`rand()` for hills; triangulate adjacent vertices into\n  quads (two triangles each).\n- WINDING — the XZ-plane gotcha: normals come from index order (CCW seen from\n  OUTSIDE = front), and on the XZ plane an angle loop `(cos a, ·, sin a)` runs\n  CLOCKWISE seen from above — the OPPOSITE of 2D XY intuition. Up-facing\n  patterns: ring fan `idx.push(center, 1 + (s+1)%seg, 1 + s)` (note: NEXT\n  angle before CURRENT); ring band `(base+s, base+sn, next+s)` +\n  `(base+sn, next+sn, next+s)`; grid quad `(i, i+cols, i+1)` +\n  `(i+1, i+cols, i+cols+1)`. Slips are safety-netted — an inside-out CLOSED\n  solid is auto-flipped (signed-volume test), and an OPEN surface (disc,\n  heightfield — anything with a boundary edge) renders double-sided — so a\n  winding mistake can't make the mesh invisible, but correct winding still\n  gives the cleanest normals/AO/shadows.\n- Faceted rock: a low-segment dome/fan of vertices, each jittered by `rand()`.\n- Low-poly tree: a `customMesh` cone of foliage over a thin `mesh('Cylinder')` trunk.\nVertices are used at their REAL world-unit size (no normalization) — generate\nthem at the final scale you want instead of building small and scaling up.\nKeep vertex counts modest (hundreds). For the faceted low-poly look, set\n`shading({ flatShading: true })` on the result (indexed meshes otherwise get\nsmooth averaged normals). Check proportions with the `size` [w,h,d] that\n`run_code`/`get_scene` report for each object — that's the ground truth for\nalignment — and save `take_screenshot()` for judging the LOOK.\nCompose several such meshes (varied scale/rotation/color) for a rich scene rather\nthan relying on a few bare primitives.\n\n## Grouping & structure\n- `group(target?)` groups the selection (or creates an Empty if nothing is\n  selected) and RETURNS the group. Group related parts so the user can move\n  them together.\n- ONE THING = ONE GROUP. The scene tree the user expects mirrors the things\n  you built, not the primitives they're made of: a table is a 'Table' group\n  containing its top and legs, a refrigerator is a 'Refrigerator' group\n  containing body/door/handle, a forest is a 'Trees' group containing the\n  scattered trunks/canopies. Build the parts with a shared name prefix\n  ('Table Top', 'Table Leg 1', …), then select by that prefix, `group()`,\n  `rename('Table')` — all in the same run_code call, since select() sees\n  objects created earlier in that call. Grouping makes every later step\n  cheaper — posing, duplicating, animating, and the user's own edits all get\n  one handle per thing instead of a flat pile at the root.\n  Independent things stay SEPARATE sibling groups (never one grab-bag), and\n  cameras/lights that serve the whole scene stay at the root.\n- COMPOUND ASSEMBLIES — build straight, group, rotate ONCE. When an object is\n  made of several primitives (a mushroom = stem + cap, a rocket = body + nose\n  + fins), build every part UNROTATED in the upright frame, stacking them with\n  `position`/`translate` so they align on a shared axis. Then group and apply\n  any tilt to the group — one rotation, applied last. Rotating each part\n  individually to pose the assembly is how parts end up misaligned: each part\n  pivots around its OWN center, so equal angles still shear the stack apart.\n  ```\n  // build the parts straight (no rotation), then select/group/pose — all in\n  // ONE call: select() sees objects created earlier in the SAME run_code.\n  mesh('Cylinder', { radius: 20, height: 120 });\n  position({ y: 60 });\n  rename('Stem');\n  mesh('Sphere', { width: 140, height: 90, depth: 140 });\n  position({ y: 140 });\n  rename('Cap');\n  select(o => o.name === 'Stem' || o.name === 'Cap');\n  const g = group();\n  rename('Mushroom');\n  rotation({ z: -15 }); // the ONLY pose rotation, on the group\n  ```\n  Per-part rotation is still right for a part's own local shape (laying a flat\n  shape down with `rotation({ x: -90 })`, turning a cylinder sideways for an\n  axle) — do that when creating the part, before grouping, not to pose the\n  whole assembly.\n- `moveInto(parent, target?)` reparents into an existing group/object (by\n  object, id, name, or 'root'); world positions are preserved. Use it to\n  organize an existing scene's tree: `const g = group(...); rename('Props');`\n  then `moveInto(g, o => o.shape('rock'));`.\n- `ungroup(target?)` dissolves a group — children hoist to its parent, the\n  empty wrapper is deleted.\n- `hide(target?)` / `show(target?)` toggle scene-tree visibility — prefer\n  hiding over deleting when the user might want something back.\n- `rename('Name')` names the selected object — always name hero objects and\n  groups so the scene tree is readable.\n- `duplicate(target?)` copies the selection in place.\n- `remove(target?)` deletes the selection (`remove('all')` clears the scene).\n\n## Suggested order of operations\n1. (existing scene) read the `<editor-context>` scene digest in the user's\n   message to learn what's there — no `get_scene()` call needed until you have\n   edited something and want to verify it.\n2. Block out hero geometry and ground.\n3. Dressing pass — scatter secondary/tertiary elements (loops, cloner\n   randomness, `cloneTo`).\n4. Color / material pass — in the believable register that means `physical`\n   shading + texture layers on the big real-world surfaces (see\n   materials-and-look), not just flat colors (or `prettify()` / `style()`).\n   Whenever two or more objects need the EXACT same material, build it once\n   and share it: `createMaterial` + `applyMaterial`, never N per-object\n   copies of the same layer stack.\n5. Lights + atmosphere (`fog`, `environment({ ao, ambient })`).\n6. Frame YOUR check with `set_view({ objects: [...] })`, `take_screenshot()`,\n   critique against the quality bar, iterate.\n7. Set the user's final presentation angle with `lookFrom()`.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}