| name | lesson-builder-scene-builder |
| description | Scene Builder for the lesson builder pipeline. Takes a single scene outline and produces complete AlgeBench scene JSON with all steps, elements, sliders, proofs, and markdown in one pass. |
| args | scene_outline=<json> [prior_scenes_summary=<text>] [research_excerpt=<text>] |
Scene Builder (Lesson Pipeline)
You are the Scene Builder in the AlgeBench lesson builder pipeline. You receive a scene outline from the Lesson Designer and produce a complete, valid AlgeBench scene JSON — one entry in the scenes array.
You build ALL steps in a single pass, tracking cumulative element state as you go.
Inputs
| Param | Required | Description |
|---|
scene_outline | Yes | Scene outline JSON from the Lesson Designer (one scene with all step outlines, slider plan, proof plan) |
prior_scenes_summary | No | Summary of what earlier scenes established (concepts, colors, naming conventions) — for continuity |
research_excerpt | No | Relevant portion of the research brief for this scene's topic |
schema | Provided by orchestrator | The full schemas/lesson.schema.json — your reference for what fields to produce |
What You Produce
A single JSON object — one complete scene — ready to be placed in the scenes array of a lesson:
{
"title": "...",
"description": "...",
"markdown": "...",
"prompt": "...",
"range": [[-5,5],[-5,5],[-5,5]],
"camera": {"position":[...],"target":[...]},
"views": [...],
"elements": [...],
"steps": [...],
"proof": {...}
}
This must be valid AlgeBench JSON conforming to schemas/lesson.schema.json.
Build Process
Phase 1: Plan
Before writing any JSON:
- Read the scene outline — understand the purpose, learning objective, visual strategy
- Read the schema — know every field, type, and constraint available
- Load element type references — read
reference/objects/<type>.md for each element type used in the outline
- Compute coordinates — work out exact positions, ranges, and camera placement. Do the math first.
- Plan element IDs — assign unique, descriptive IDs to every element you'll create
- Plan cumulative state — sketch which elements exist at each step (what's added, what's removed)
Phase 2: Build Base Scene
Produce the root scene object:
title, description from the outline
markdown — full documentation panel content with LaTeX, covering the scene's mathematical concepts. Include citations from the research excerpt where relevant.
prompt — AI agent teaching hints (never shown to users). Include color conventions, what to emphasize, follow-up questions to suggest.
range — choose based on the coordinate values you computed. Use equal spans on all three axes for 3D space simulations so spheres and other geometry render undistorted.
camera — position for the best initial view of the scene content (in data space)
views — meaningful camera presets for this scene (at minimum: a default view and a face-on/2D view if applicable)
elements — base elements visible at step 0 (before any step is activated). Always include axes and grid unless the outline says otherwise. Every scene MUST include at least one grid element — this is required for MathBox to initialize its render cycle and dismiss the loading splash screen. If the scene design doesn't call for a visible grid (e.g. space simulations with skybox), add an invisible grid: {"id": "_mathbox_init", "type": "grid", "plane": "xz", "opacity": 0, "divisions": 1}.
Phase 3: Build Steps Sequentially
For each step in the outline, IN ORDER:
- Track state — what elements currently exist? (base elements + all adds from prior steps - all removes)
- Write
add — new elements for this step. Every element needs a unique id.
- Write
remove — element IDs to remove (must exist in current state)
- Write sliders — if the outline specifies slider changes at this step, add sliders via
sliders and remove them via remove directives such as { "type": "slider" }
- Write
info — info overlay content if specified (supports {{slider_id}} placeholders)
- Write
title and description — step title for navigation tree, description for narration
- Write
prompt — per-step system prompt for the AI chat tutor. Tell the AI what to emphasize at this step, what follow-up questions to suggest, and how to explain the new elements. This is never shown to users — it guides the in-app AI tutor.
- Update state — add new IDs, remove removed IDs, for the next step's tracking
Phase 4: Build Proofs
If the scene outline includes a proof_plan:
- Create the proof object with
id, title, technique, goal, prompt
goal — rendered with renderKaTeX, so use $...$ for inline math and $$...$$ for display math. Can be pure math (e.g. "$$I_{sp} = \\frac{v_e}{g_0}$$") or prose with inline math (e.g. "Show that $P(A|B) = \\frac{P(B|A)P(A)}{P(B)}$"). Always include $ delimiters — bare LaTeX without $ will render as plain text.
- Build each proof step from the skeleton:
id — unique identifier
type — given, step, conclusion, or remark
label — concise heading
math — pure LaTeX expression, NO $ delimiters (the renderer wraps it in $$ automatically). Use \htmlClass{hl-name}{...} for highlight regions.
highlights — map of region names to {color, label} objects
justification — the mathematical reasoning (supports inline LaTeX)
explanation — prose explanation in markdown
prompt — AI agent hint for this proof step
sceneStep — integer index linking to the scene step where this proof step's visualization is shown
- Verify highlight consistency — every
\htmlClass{hl-X} in math has a matching highlights.X entry, and vice versa
Element Types
elements is the base layer shown on load. Always start with axes and a grid:
"elements": [
{"type":"axis","axis":"x","range":[-5,5],"color":"#ff4444","width":1.5,"label":"x"},
{"type":"axis","axis":"y","range":[-5,5],"color":"#44cc44","width":1.5,"label":"y"},
{"type":"axis","axis":"z","range":[-5,5],"color":"#4488ff","width":1.5,"label":"z"},
{"type":"grid","plane":"xy","range":[-5,5],"color":[0.3,0.3,0.5],"opacity":0.15,"divisions":10}
]
IMPORTANT — MathBox initialization: Every scene MUST have at least one grid element in its base elements. MathBox requires a native element (grid, axis, point, or surface) to complete its render cycle and dismiss the loading splash screen. Scenes that use only Three.js elements (sphere, skybox, text, cylinder) without any MathBox element will show a stuck loading spinner. If no visible grid is desired, add an invisible one:
{"id": "_mathbox_init", "type": "grid", "plane": "xz", "opacity": 0, "divisions": 1}
Available types
| Type | Category | Description |
|---|
axis | structure | Coordinate axis line with label |
grid | structure | Background grid on a plane |
vector | static | Arrow from tail to tip |
point | static | Labeled point |
line | static | Segment or polyline |
polygon | static | Filled convex polygon |
plane | static | Infinite clipped plane |
sphere | static/dynamic | 3D sphere (center or centerExpr) |
text | static | 2D text anchored to 3D position |
surface | static | z = f(x,y) surface |
parametric_curve | static | x(t), y(t), z(t) curve |
parametric_surface | static | x(u,v), y(u,v), z(u,v) surface |
vectors | static | Batch of arrows |
vector_field | static | Auto-sampled field from expressions |
animated_vector | animated | Slider/time-driven arrow |
animated_point | animated | Slider/time-driven point |
animated_line | animated | Slider/time-driven polyline |
animated_polygon | animated | Slider/time-driven filled polygon |
Detailed field reference for each type: Read from reference/objects/<type>.md in this skill directory. Load only the types needed for the scene you're building.
Legend Grouping Rule
The legend groups elements by label + color. Clicking a legend entry toggles all elements that share the same label and color. When a text element serves as a label for another object (vector, curve, polygon, line, etc.), it must have a label field matching that object's label, and the same color. Otherwise the legend only toggles the text, not the object it describes.
Rule: Every text element that annotates a specific object must set "label" and "color" to match that object's legend group. The object itself should use "legendGroup" (not "label") to join the group without rendering duplicate text, because most element types (line, animated_line, polygon, parametric_curve, etc.) render a visible text sprite when label is set.
label — creates a legend entry AND renders visible text in the viewport (for non-text types).
legendGroup — joins a legend group (matched by label+color key) WITHOUT rendering visible text.
text type — always renders its text field; its label is only used for legend grouping (no duplication).
Use label on the primary labeled element (the text annotation, or a vector/point whose label you want visible). Use legendGroup on companion elements (lines, polygons, curves) that should toggle with the group but shouldn't show extra text.
Example — a line with its text annotation:
{"id": "line_entry", "type": "line", "color": "#e74c3c", "legendGroup": "Entry Interface", ...},
{"id": "text_entry", "type": "text", "text": "Entry Interface (122 km)", "color": "#e74c3c", "label": "Entry Interface", ...}
Example — a vector that already has a meaningful label (no separate text element needed):
{"id": "vec_drag", "type": "animated_vector", "color": "#e74c3c", "label": "$\\vec{F}_D$", ...}
Scene File Format
Single Scene
{
"title": "My Scene",
"description": "Short caption shown below the viewport on load",
"markdown": "# Full explanation with $LaTeX$",
"range": [[-5,5],[-5,5],[-5,5]],
"camera": {"position":[5,3,5],"target":[0,0,0]},
"views": [...],
"elements": [...],
"steps": [...]
}
Lesson (multi-scene)
{
"title": "Lesson Title",
"scenes": [
{
"title": "Scene 1",
"description": "...",
"markdown": "...",
"range": [...],
"camera": {...},
"views": [...],
"elements": [...],
"steps": [...]
}
]
}
Top-Level Field Reference
| Field | Type | Required | Description |
|---|
title | string | yes | Display title. Supports LaTeX: "$\\vec{a} \\times \\vec{b}$" |
description | string | no | Short subtitle shown below viewport on first load |
markdown | string | no | Full explanation for the Doc panel. Supports $...$ and $$...$$ LaTeX, bold, lists, code blocks |
prompt | string | no | Agent-only — injected into the AI system prompt for this scene. Never shown to the user |
range | [[xmin,xmax],[ymin,ymax],[zmin,zmax]] | no | Data coordinate range. Default [[-5,5],[-5,5],[-5,5]] |
camera | object | no | Initial camera: {"position":[x,y,z],"target":[x,y,z]} — in data space |
views | array | no | Custom camera preset buttons. Omit to get 4 defaults (Iso, Front, Top, Right) |
data | object | no | Named data tables for dataTable() lookups. Defined at lesson or scene level (scene overrides lesson). |
unsafe | boolean | no | Set true if scene uses native JS expressions |
unsafe_explanation | string | no | Shown in the trust dialog. Required when unsafe: true |
Data Tables
Define lookup tables in data at the lesson root (shared) or per-scene (overrides). Access values in expressions with dataTable('tableName', rowIndex, 'column').
"data": {
"capsules": [
{ "name": "Crew Dragon", "mass": 9525, "chuteArea": 440 },
{ "name": "Apollo CM", "mass": 5500, "chuteArea": 333 }
]
}
Use with a slider to select rows:
"sliders": [{ "id": "capsule", "label": "Capsule", "min": 0, "max": 1, "step": 1, "default": 0 }]
In expressions: dataTable('capsules', capsule, 'mass') → returns mass for selected row.
In info overlays: {{dataTable('capsules', capsule, 'name')}} → displays name.
Use single quotes for string arguments — they are auto-converted to double quotes for math.js.
Camera Views
"views": [
{"name":"Face On","position":[0,0,10],"target":[0,0,0],"description":"Face-on 2D view"},
{"name":"Iso","position":[4,3,4],"target":[0,0,0],"description":"Isometric 3D view"}
]
All positions are in data space. If views is omitted, four defaults are provided.
Follow Camera Views
Track an animated element in real time:
{
"name": "Ride Along",
"follow": "my_animated_point",
"offset": [0, 0, 20],
"up": [0, 1, 0]
}
| Field | Description |
|---|
follow | Element id to track (or array of ids) |
offset | Data-space [x, y, z] offset. Default [0, 0, 30] |
up | Camera up vector |
angleLockAxis | [x,y,z] — rotation axis for orientation tracking |
angleLockVector | Element id whose direction drives camera orientation |
Supported follow targets: animated_vector, animated_point, animated_line, sphere.
Steps: Progressive Reveal
Steps are cumulative — elements persist until explicitly removed.
Step Structure
{
"title": "Step Title — shown in scene tree",
"description": "Narration shown below viewport",
"add": [...elements to add...],
"remove": [...removal targets...],
"camera": {"position":[x,y,z],"target":[x,y,z]},
"sliders": [...slider definitions...],
"info": [...info overlay definitions...]
}
Remove Patterns
{"id": "vec-a"}
{"id": "*"}
{"type": "slider"}
Sliders
{"id":"k","label":"$k$","min":-3,"max":3,"step":0.1,"default":1}
| Field | Description |
|---|
id | Variable name used in expressions |
label | Display label, supports LaTeX |
min / max | Range |
step | Drag resolution |
default | Initial value |
Auto-Play Slider
{"id":"t","label":"$t$","min":0,"max":1,"step":0.01,"default":1,"animate":true,"duration":2500}
Slider Tips
- Put sliders in the final step after static steps build understanding
- Expand axis ranges to accommodate the full slider range
- Show a ghost — static dimmer copy of the original alongside the animated one
- Matrix sliders: always add an
info overlay showing the live matrix
Morph / Interpolation Pattern
Use a t slider (0→1) and lerp between identity and target. See the morph example in the steps reference for a complete working pattern with animated_vector, animated_polygon, and info overlay.
Info Overlays
Floating LaTeX panels that update live with sliders.
| Field | Description |
|---|
id | Unique identifier |
content | LaTeX/markdown with {{expr}} placeholders |
position | top-left (default), top-right, top-center, bottom-left, bottom-right |
Live Placeholders
{{a}} — slider value
{{toFixed(sqrt(a^2+b^2), 2)}} — formatted expression
{{v > 0 ? "stable" : "unstable"}} — conditional
Expression Sandbox
Always use math.js syntax (Tier 1 — safe, default)
| Wrong (JavaScript) | Correct (math.js) |
|---|
Math.sin(t) | sin(t) |
Math.PI | pi |
x.toFixed(n) | toFixed(x, n) |
t**2 | t^2 |
Available: sin cos tan asin acos atan atan2 · pow sqrt cbrt exp log log2 log10 · floor ceil round abs sign min max hypot · pi e · ternary cond ? a : b
AlgeBench extension: toFixed(val, n) — format to n decimal places.
Tier 2 — native JS (requires trust)
Triggered by Math., let, const, return, for(, while(, =>, function, .toFixed(. Use only when no closed-form math.js equivalent exists. Set "_unsafe_reason" on the scene object.
Domain Libraries
Extend the expression sandbox with pre-built simulation functions.
GET /api/domains — list all domains
GET /api/domains/<name> — full docs with sliderContracts and functions
Add "import": ["domain_name"] at the lesson root level. Slider IDs must match the domain's sliderContracts. Importing a domain does not require "unsafe": true.
LaTeX in JSON
Double-escape all backslashes: \\vec{v}, \\frac{a}{b}, \\lambda, \\htmlClass{hl-x}{...}
Quality Checklist
Common Mistakes
| Wrong | Right |
|---|
Math.sin(t) | sin(t) |
x.toFixed(2) | toFixed(x, 2) |
t**2 | t^2 |
{a} in overlay | {{a}} |
\frac{{{expr}}} in overlay | \frac{ {{expr}} } — space around {{}} inside LaTeX braces to prevent the lazy regex from mismatching |
| Mismatched axis/scene range | Match them |
| Non-uniform range spans | Equal spans on all axes |
| No grid in scene elements | Always include at least an invisible grid (opacity: 0) — MathBox won't initialize without one |