Skip to main content

geop_core_editor/agent/
mod.rs

1//! An [`Editor`] driven by an agent's tools: the one tool layer of every
2//! front end an agent talks to — the MCP server (`geop mcp`) and the web
3//! app's hosted assistant alike.
4//!
5//! Each tool ([`tools`]) is a transaction on one editor through its
6//! protocol ([`Command`] in, [`Update`] out): a part is started or loaded,
7//! steps are added, edited and removed as their saved JSON (what a `.geop`
8//! file holds), the program is run, its elements listed by the stable names
9//! a step refers to them by, inspected, exported and saved. None of them
10//! names an operation: what there is to build, and how, is the registry's,
11//! and the agent asks for it (`list_operations`).
12//!
13//! The tools that read or write files by path ([`AgentTool::files`]) need
14//! the [`Files`] of the front end they run in; one with none — the browser,
15//! whose downloads are the user's to start — does not offer them.
16//!
17//! A [`Session`] keeps what a front end keeps of the editor's answers — the
18//! last scene, the program as last shown — whoever sent the command, so
19//! that a front end driving the editor itself (the web app's own clicks)
20//! and an agent's tools can share it.
21
22use std::collections::BTreeMap;
23
24use geop_core_math::{
25    geop_error::{GeopError, GeopResult},
26    scalars::Scalar,
27    vector::Vector3,
28};
29use geop_core_program::{
30    element::PickedElement,
31    placed::Located,
32    program::{Program, ProgramStep},
33    values::{StateValue, StateValues},
34};
35use serde::Serialize;
36use serde_json::{Value, json};
37
38mod resources;
39
40pub use resources::{AgentPrompt, AgentResource, Contents, prompts, resource_templates, resources};
41
42use crate::{
43    editing::{Pointer, Presentation, StepEditEvent, Value as DialogValue},
44    editor::{Command, Editor, ProgramState, SceneState, StepState, Update},
45    view::{
46        PartView,
47        lit::Lighting,
48        picture::{Camera, picture},
49    },
50};
51
52/// A tool an agent is offered: its name, what it does and the JSON schema
53/// of its input.
54#[derive(Clone, Debug, PartialEq)]
55pub struct AgentTool {
56    pub name: &'static str,
57    pub description: &'static str,
58    pub input_schema: Value,
59    /// Whether it reads or writes the front end's files by path (see
60    /// [`Files`]): offered only where there are such files — not in the
61    /// browser, where a file is saved only when the user says so.
62    pub files: bool,
63}
64
65impl AgentTool {
66    /// As MCP's `tools/list` describes it.
67    pub fn mcp(&self) -> Value {
68        json!({ "name": self.name, "description": self.description, "inputSchema": self.input_schema })
69    }
70
71    /// As the Anthropic Messages API takes it (`tools`).
72    pub fn anthropic(&self) -> Value {
73        json!({ "name": self.name, "description": self.description, "input_schema": self.input_schema })
74    }
75}
76
77/// Every tool, in the order they are offered.
78pub fn tools() -> Vec<AgentTool> {
79    let tool = |name, description, properties: Value, required: &[&str], files| AgentTool {
80        name,
81        description,
82        input_schema: json!({
83            "type": "object",
84            "properties": properties,
85            "required": required,
86            "additionalProperties": false,
87        }),
88        files,
89    };
90    let string = |description: &str| json!({ "type": "string", "description": description });
91    let object = |description: &str| json!({ "type": "object", "description": description });
92    vec![
93        tool(
94            "new_part",
95            "Start a new, empty part. Every part has the coordinate-system datum `origin`, whose planes a sketch is drawn on, as `add_sketch`'s `plane`: {\"datum\": \"origin:xy\"}, {\"datum\": \"origin:yz\"} or {\"datum\": \"origin:zx\"}. A point (a, b) of a sketch lies in the world at (a, b, 0) on `origin:xy` (normal +z), (0, a, b) on `origin:yz` (normal +x), and (a, 0, -b) on `origin:zx` (normal +y): there, the sketch's y runs along -z. A sketch's normal is the way an extent goes (see `add_step`). y is up in the world.",
96            json!({}),
97            &[],
98            false,
99        ),
100        tool(
101            "load_program",
102            "Replace the part with a program: the `.geop` file at `path` (the other `.geop` files next to it are read too, for the parts it places), or the program `text` itself.",
103            json!({ "path": string("A .geop file on disk."), "text": string("A program as JSON, as a .geop file holds it.") }),
104            &[],
105            true,
106        ),
107        tool(
108            "list_operations",
109            "Every operation a step can be: its kind (what `add_step` takes), label, group and what it does. Given a `kind`, that operation alone, with the arguments a new step starts with (`default_args`, the JSON `add_step` takes) and the form the editor shows for it (`form`: each field's key, control, label, value and, for a reference, the kinds of elements it accepts).",
110            json!({ "kind": string("The operation to describe in full, e.g. `extrude`.") }),
111            &[],
112            false,
113        ),
114        tool(
115            "add_step",
116            "Add a step of the operation `kind` with `args` (its saved JSON, see `list_operations` with `kind`) at the end of the program, or before the step `before`. A step whose arguments do not read is refused and not added; one that reads but does not build is added, and its error says why. Answers with every step's status: an error names the step and what went wrong, and `moved` lists the parts the solver moved from where they were loaded or saved, the furthest first: mates that hold only because a part jumped are no error, so read it. An extent runs from the sketch's plane: `side1` along the normal (against it if `reversed` or negative) and `side2` the other way, a negative `side2` being that far along the normal, so that the solid starts away from the plane. Its `start` cap is at the `side2` end (the plane, with no `side2`) and its `end` cap at the `side1` end, whichever way that is in the world.",
117            json!({ "kind": string("The operation, e.g. `add_sketch`, `extrude`, `fillet`."), "args": object("The step's arguments as saved in a .geop file."), "id": string("The step's id (letters, digits, `_`, `-`, `.`); chosen from the operation's label if left out. Later steps refer to what this one builds by it, e.g. `extrude(<id>)`."), "before": string("The step to add it before; the end of the program if left out.") }),
118            &["kind", "args"],
119            false,
120        ),
121        tool(
122            "edit_step",
123            "Change the step `id`: the keys of `args` replace those of its arguments (the others stay), so one dimension can be changed alone. The program is rebuilt from that step on.",
124            json!({ "id": string("The step to change."), "args": object("The arguments to set.") }),
125            &["id", "args"],
126            false,
127        ),
128        tool(
129            "delete_step",
130            "Remove the step `id`. Steps that referred to what it built then fail, and say so.",
131            json!({ "id": string("The step to remove.") }),
132            &["id"],
133            false,
134        ),
135        tool(
136            "run",
137            "Run the whole program and report every step: its summary, whether it ran, and its error with the kernel's context if it failed; and the solids the part has.",
138            json!({}),
139            &[],
140            false,
141        ),
142        tool(
143            "list_elements",
144            "The names of the part's elements as they are now, for typed references: solids, faces (with their solid), edges (with their faces), vertices, sketches, datums and what else the part lists (threads, placed parts). A face is referred to by its name, as a sketch's plane {\"face\": ...}; an edge by its name, as in a fillet's `edges`. In a swept feature's names, `start` and `end` say which cap: `extrude(box,end)` is the end cap, `extrude(box,sketch,c4,end)` the edge of the side face swept by the curve c4 on that cap, and `extrude(box,sketch,p1,end)` the vertex over the point p1. Given `placed` (a placed part's path, `hip` or `hip/servo`), the elements of that part instead, in its own frame, with where it is placed (`frame`), its size (`extent`), its datums (the frames to mount by) and the `target` to refer to them by.",
145            json!({ "placed": string("The placed part to list, e.g. `hip/servo`.") }),
146            &[],
147            false,
148        ),
149        tool(
150            "inspect",
151            "Ask about the part: `mass_properties` (volume, area and centroid of every solid; its mass and inertia if its part has a material, null if not), `interference` (which solids overlap), or `measure` with `elements` (a point, a length, an area, a radius; the distance and angle between two). On an assembly the first two can take minutes, and calls wait their turn behind them: narrow them with `options` {\"parts\": [\"left_leg\", \"hip/servo\"]}, the solids of those placed parts and what is placed in them (`interference`: each against everything, or, with \"only_among\": true, only against each other), and give `limit_seconds`: past it, the answer is what was found so far, with `stopped` saying how far it got. A cancelled call stops likewise, at the next pair or solid.",
152            json!({ "inspector": { "type": "string", "enum": ["mass_properties", "interference", "measure"] }, "elements": { "type": "array", "description": "For `measure`: one or two elements, each {\"kind\": \"vertex\"|\"edge\"|\"face\"|\"datum\", \"name\": ...}.", "items": { "type": "object", "properties": { "kind": string("vertex, edge, face or datum"), "name": string("Its name, from list_elements") }, "required": ["kind", "name"] } }, "options": object("The inspector's own options: `parts`, `only_among`."), "limit_seconds": { "type": "number", "description": "How long it may take, where the front end keeps time (`geop mcp`; not in the browser)." } }),
153            &["inspector"],
154            false,
155        ),
156        tool(
157            "export",
158            "Write what the program builds to `path`, in the format its extension names unless `format` says otherwise: stl, step, svg or dxf (drawings and flat patterns), csv (bill of materials), zip (URDF).",
159            json!({ "path": string("Where to write the file."), "format": string("The format, if not the path's extension."), "options": object("The exporter's own options, e.g. {\"quality\": 8} for STL."), "limit_seconds": { "type": "number", "description": "How long an STL of placed parts may take: past it, the export fails and nothing is written." } }),
160            &["path"],
161            true,
162        ),
163        tool(
164            "save",
165            "Write the program to `path` as a .geop file, and make that the file it is edited in.",
166            json!({ "path": string("Where to write the program.") }),
167            &["path"],
168            true,
169        ),
170        tool(
171            "look",
172            "A picture of the part as the editor draws it — the placed parts, what is lit, and what the step open draws over it — as a PNG, seen orthographically from the view `view` (`iso`, `front` (from +z), `back`, `right` (from +x), `left`, `top` (from +y), `bottom`) or along `look`, with `up` up; with neither, as last seen, or, while a step works in a plane (a sketch's), facing it. y is up in the world. Framed to all of it. Its camera is what `step_event`'s pixels are in: answers with it, and where the world's origin is in it.",
173            json!({ "view": { "type": "string", "enum": VIEWS.map(|(name, ..)| name) }, "look": { "type": "array", "items": { "type": "number" }, "description": "The direction to look along, [x, y, z]." }, "up": { "type": "array", "items": { "type": "number" }, "description": "Which way is up in the picture, [x, y, z]; y, if not given." }, "width": { "type": "integer", "description": "In pixels; 800 if not given." }, "height": { "type": "integer", "description": "In pixels; 600 if not given." } }),
174            &[],
175            false,
176        ),
177        tool(
178            "undo",
179            "Go back one edit of the program — while a step is open, one edit of that step — as the editor's Ctrl+Z does.",
180            json!({}),
181            &[],
182            false,
183        ),
184        tool(
185            "redo",
186            "Make the edit `undo` went back from again.",
187            json!({}),
188            &[],
189            false,
190        ),
191        tool(
192            "open_step",
193            "Open a step to edit it the way a user does in the editor — the step `id`, or a new step of the operation `kind` where new steps go — and keep it open across calls, for `step_event`, until `commit_step` or `cancel_step`. Given `placed` (a placed part's path, `neck` or `neck/head`), it is that placed part's step, or a new step of it, edited where it is placed. Answers with the step as the editor shows it: its form (each field's key, label and value), what it still needs picked, what a click picks now, a value asked for in place, and its error. While a step is open, the tools that change the program are refused.",
194            json!({ "id": string("The step to open."), "kind": string("The operation of a new step, e.g. `add_sketch`."), "placed": string("The placed part whose step it is.") }),
195            &[],
196            false,
197        ),
198        tool(
199            "step_event",
200            "Do something to the open step, as a user does in the editor — or, with no step open, with the tool in hand (with none taken, the drag tool: dragging a placed part moves it as far as its mates let it). `event.type` is one of: `dialog` (set the form's field `key` to `value`), `click` (at `pointer`; `double`, `shift`), `hover` (move the pointer to `pointer` with no button held), `drag` (press at `from`, move to `to` in `steps` moves, release), `select_box` (a box from the pixel `from` to the pixel `to`: left to right selects what lies wholly in it, right to left what it touches; `add` keeps the selection), `key` (a key as a browser names it: `l` for the sketch's line tool, `Escape`, `Enter`, `Delete`) or `leave` (the pointer leaves the view). A pointer is `{\"pixel\": [x, y]}` in the last picture `look` took, or `{\"at\": [x, y, z]}`, a point of the world seen along that picture's view (while a step works in a plane, a sketch's, the view faces it head on), with `cycle` to mean the n-th of what lies under it, front to back. Answers with the step or the tool as it then is.",
201            json!({ "event": { "type": "object", "description": "The event: {\"type\": \"dialog\", \"key\": ..., \"value\": ...}, {\"type\": \"click\", \"pointer\": {...}}, {\"type\": \"drag\", \"from\": {...}, \"to\": {...}}, {\"type\": \"key\", \"key\": \"Escape\"}, ...", "properties": { "type": { "type": "string", "enum": ["dialog", "click", "hover", "drag", "select_box", "key", "leave"] } }, "required": ["type"] } }),
202            &["event"],
203            false,
204        ),
205        tool(
206            "commit_step",
207            "Put the open step into the program, as the dialog's OK does: refused, and the step kept open, unless it builds.",
208            json!({}),
209            &[],
210            false,
211        ),
212        tool(
213            "cancel_step",
214            "Close the open step and leave the program as it was.",
215            json!({}),
216            &[],
217            false,
218        ),
219        tool(
220            "take_tool",
221            "Take the tool `key` in hand, as the editor's toolbar does, or put the one in hand down (no `key`). With no step open, `step_event` is the tool's: `pick:<inspector>` (e.g. `pick:measure`) picks elements to ask that inspector about, its answer coming with every event. `run` lists the tools there are.",
222            json!({ "key": string("The tool, e.g. `pick:measure`; none puts the tool in hand down.") }),
223            &[],
224            false,
225        ),
226        tool(
227            "panel",
228            "What the panel `key` shows of the program — an assembly's joints and how free each placed part is, its configurations — or, given `payload`, something done in it first, as the panel reads it: `{\"command\": ..., ...}`. A payload the panel does not read is refused with what it expects. `run` lists the panels there are.",
229            json!({ "key": string("The panel."), "payload": { "description": "What was done in it, as the panel reads it." } }),
230            &["key"],
231            false,
232        ),
233    ]
234}
235
236/// The views `look` names, as the editor's view cube does: where the eye
237/// is from what it looks at, and which way is up. Isometric is the
238/// editor's first view.
239const VIEWS: [(&str, [f64; 3], [f64; 3]); 7] = [
240    ("iso", [66.0, 44.0, 88.0], [0.0, 1.0, 0.0]),
241    ("front", [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]),
242    ("back", [0.0, 0.0, -1.0], [0.0, 1.0, 0.0]),
243    ("right", [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
244    ("left", [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
245    ("top", [0.0, 1.0, 0.0], [0.0, 0.0, -1.0]),
246    ("bottom", [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]),
247];
248
249/// How far a part has to have moved, in length or radians, for the status
250/// to say so: well above what a solve leaves of a part that stayed.
251const MOVED: f64 = 1e-6;
252
253/// The size of a picture, unless `look` is told otherwise.
254const PICTURE: (u32, u32) = (800, 600);
255
256/// The tools that change the program, refused while a step is open: what
257/// they would do is not the open step's, and would leave it behind.
258const CHANGE_THE_PROGRAM: [&str; 6] = [
259    "new_part",
260    "load_program",
261    "add_step",
262    "edit_step",
263    "delete_step",
264    "open_step",
265];
266
267/// The files of the front end a session runs in, which the tools that
268/// take a path read and write ([`AgentTool::files`]).
269pub trait Files {
270    /// The text of the file `path`.
271    fn read(&mut self, path: &str) -> GeopResult<String>;
272    /// The program files in the folder of `path` — `path` among them — by
273    /// path, with their text: the files the program at `path` places parts
274    /// from.
275    fn programs_beside(&mut self, path: &str) -> BTreeMap<String, Option<String>>;
276    /// Writes `bytes` to the file `path`.
277    fn write(&mut self, path: &str, bytes: &[u8]) -> GeopResult<()>;
278    /// Today, `YYYY-MM-DD`: what an export is dated.
279    fn today(&self) -> String;
280}
281
282/// What a tool answered: its result as text — pretty JSON, or why it
283/// failed — and every update the editor sent while it ran, in order, for a
284/// front end that shows the editor to show.
285#[derive(Clone, Debug, Serialize)]
286pub struct ToolAnswer {
287    pub text: String,
288    pub is_error: bool,
289    pub updates: Vec<Update>,
290    /// The pictures it answered with too (`look`).
291    pub images: Vec<Image>,
292}
293
294/// A picture a tool answered with.
295#[derive(Clone, Debug, PartialEq, Serialize)]
296pub struct Image {
297    pub media_type: &'static str,
298    /// Its bytes, as base64.
299    #[serde(serialize_with = "as_base64")]
300    pub data: Vec<u8>,
301}
302
303impl Image {
304    /// Its bytes as base64, as JSON carries them.
305    pub fn base64(&self) -> String {
306        use base64::Engine;
307        base64::engine::general_purpose::STANDARD.encode(&self.data)
308    }
309}
310
311fn as_base64<S: serde::Serializer>(data: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
312    use base64::Engine;
313    serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(data))
314}
315
316/// One editor, driven by tools and by a front end alike, and what a front
317/// end keeps of its answers.
318pub struct Session {
319    editor: Editor,
320    /// The part as last drawn: the names `list_elements` answers with.
321    scene: Option<SceneState>,
322    /// The program as last shown: its steps' status, the operations.
323    shown: Option<ProgramState>,
324    /// The step being edited, as last shown.
325    step: Option<StepState>,
326    /// What the tool in hand shows, while no step is edited.
327    tool: Option<Presentation>,
328    /// What is lit, as last sent.
329    lit: Option<Lighting>,
330    /// The program as last known to be whole, to start a fresh editor from
331    /// after a panic ([`Self::restart`]).
332    program: Program,
333    /// The file it is edited in, once told.
334    path: Option<String>,
335    /// The updates of the tool running, in order.
336    sent: Vec<Update>,
337    /// The pictures of the tool running.
338    images: Vec<Image>,
339    /// What the last picture saw, which pixels are in: none until `look`,
340    /// or since the step open changed.
341    camera: Option<Camera>,
342    /// Whether the step open has been told which way is up in `camera`.
343    told: bool,
344    /// The program's state as it was loaded or saved, which the status
345    /// says what moved from.
346    saved: StateValues,
347}
348
349impl Session {
350    /// A session of `editor`, shown whole.
351    pub fn new(editor: Editor) -> Self {
352        let mut session = Self {
353            editor,
354            scene: None,
355            shown: None,
356            step: None,
357            tool: None,
358            lit: None,
359            program: Program::new(),
360            path: None,
361            sent: Vec::new(),
362            images: Vec::new(),
363            camera: None,
364            told: false,
365            saved: StateValues::new(),
366        };
367        session.handle(Command::Show);
368        // That scene was the session's to know, not the viewer's to have:
369        // the front end's first command gets all of it.
370        session.editor.forget_viewer();
371        session
372    }
373
374    /// The editor.
375    pub fn editor(&self) -> &Editor {
376        &self.editor
377    }
378
379    /// Applies `command`, as a front end sends it, keeping what the session
380    /// keeps of the answer.
381    pub fn handle(&mut self, command: Command) -> Update {
382        let path = match &command {
383            Command::Load {
384                path: Some(path), ..
385            } => Some(path.clone()),
386            _ => None,
387        };
388        let update = self.editor.handle(command);
389        if let Some(scene) = &update.scene {
390            // What a scene does not send again is as the last sent it.
391            let mut scene = scene.clone();
392            if let Some(last) = self.scene.take() {
393                scene.hidden = scene.hidden.or(last.hidden);
394                scene.structure = scene.structure.or(last.structure);
395                // The placed parts not sent again are as last sent, in
396                // the colours they were given then.
397                if !scene.all {
398                    let sent: Vec<String> =
399                        scene.instances.iter().map(|i| i.name.clone()).collect();
400                    let kept = (last.instances.into_iter())
401                        .filter(|i| !sent.contains(&i.name) && !scene.removed.contains(&i.name));
402                    scene.instances.extend(kept);
403                }
404            }
405            self.scene = Some(scene);
406        }
407        if let Some(program) = &update.program {
408            self.shown = Some(program.clone());
409        }
410        // A step opened or closed, or one starting to work in a plane, is
411        // seen afresh: facing that plane.
412        let seen = |step: &Option<StepState>| {
413            (step.as_ref()).map(|s| (s.kind, s.id.clone(), s.presentation.focus.is_some()))
414        };
415        if seen(&self.step) != seen(&update.step) {
416            self.camera = None;
417            self.told = false;
418        }
419        self.step = update.step.clone();
420        self.tool = update.tool.clone();
421        if let Some(lit) = &update.lit {
422            self.lit = Some(lit.clone());
423        }
424        if update.error.is_none() {
425            self.program = self.editor.program().clone();
426            if path.is_some() {
427                self.path = path;
428            }
429        }
430        update
431    }
432
433    /// [`Self::handle`] for a command and an update as JSON.
434    pub fn handle_json(&mut self, command: &str) -> Result<String, String> {
435        let command: Command =
436            serde_json::from_str(command).map_err(|e| format!("reading command: {e}"))?;
437        serde_json::to_string(&self.handle(command)).map_err(|e| format!("writing update: {e}"))
438    }
439
440    /// Replaces an editor left in an unknown state — a kernel that panicked
441    /// — with `editor`, on the program as it was last whole.
442    pub fn restart(&mut self, editor: Editor) {
443        self.editor = editor;
444        self.scene = None;
445        self.shown = None;
446        self.lit = None;
447        let program = self.program.clone();
448        let path = self.path.clone();
449        self.handle(Command::Load { program, path });
450    }
451
452    /// Calls the tool `name` with `input`, in a front end with `files`, if
453    /// it has any: its result as pretty JSON, or why it failed.
454    pub fn call(&mut self, name: &str, input: &Value, files: Option<&mut dyn Files>) -> ToolAnswer {
455        self.sent.clear();
456        self.images.clear();
457        let (text, is_error) = match self.tool(name, input, files) {
458            Ok(result) => (
459                serde_json::to_string_pretty(&result).unwrap_or_else(|_| result.to_string()),
460                false,
461            ),
462            Err(e) => (format!("{name} failed: {e}"), true),
463        };
464        ToolAnswer {
465            text,
466            is_error,
467            updates: std::mem::take(&mut self.sent),
468            images: std::mem::take(&mut self.images),
469        }
470    }
471
472    /// Sends `command` for a tool: the update, or its error as an `Err`.
473    fn send(&mut self, command: Command) -> GeopResult<Update> {
474        let update = self.handle(command);
475        self.sent.push(update.clone());
476        match update.error {
477            Some(error) => Err(GeopError::new(error)),
478            None => Ok(update),
479        }
480    }
481
482    /// The tool `name` applied to `args`: what it answers.
483    fn tool(
484        &mut self,
485        name: &str,
486        args: &Value,
487        files: Option<&mut dyn Files>,
488    ) -> GeopResult<Value> {
489        let text = |key: &str| -> GeopResult<String> {
490            args.get(key)
491                .and_then(Value::as_str)
492                .map(str::to_string)
493                .ok_or_else(|| GeopError::new(format!("`{key}` (a string) is required")))
494        };
495        let offered = tools().into_iter().find(|tool| tool.name == name);
496        let files = match (offered, files) {
497            (None, _) => return Err(GeopError::new(format!("there is no tool `{name}`"))),
498            (Some(tool), None) if tool.files => {
499                return Err(GeopError::new(format!(
500                    "`{name}` is not offered here: it reads or writes files by path, and there are none"
501                )));
502            }
503            (_, files) => files,
504        };
505        if CHANGE_THE_PROGRAM.contains(&name)
506            && let Some(step) = &self.step
507        {
508            return Err(GeopError::new(format!(
509                "the step {} ({}) is open: commit_step or cancel_step first",
510                step.id.as_deref().unwrap_or("being added"),
511                step.kind
512            )));
513        }
514        match (name, files) {
515            ("new_part", _) => {
516                self.saved = StateValues::new();
517                self.load(Program::new())?;
518                self.status()
519            }
520            ("load_program", Some(files)) => {
521                if let Some(path) = args.get("path").and_then(Value::as_str) {
522                    self.load_file(path, files)
523                } else {
524                    let program = Program::from_json(&text("text")?)?;
525                    self.saved = program.state.clone();
526                    self.load(program)?;
527                    self.status()
528                }
529            }
530            ("list_operations", _) => {
531                self.list_operations(args.get("kind").and_then(Value::as_str))
532            }
533            ("add_step", _) => {
534                let kind = text("kind")?;
535                let step_args = (args.get("args").cloned())
536                    .ok_or_else(|| GeopError::new("`args` (the step's arguments) is required"))?;
537                let mut program = self.editor.program().clone();
538                let id = match args.get("id").and_then(Value::as_str) {
539                    Some(id) => id.to_string(),
540                    None => program.fresh_id(&kind),
541                };
542                let at = match args.get("before").and_then(Value::as_str) {
543                    Some(before) => program.index_of(before)?,
544                    None => program.steps.len(),
545                };
546                let step = ProgramStep {
547                    id: id.clone(),
548                    operation: kind,
549                    args: step_args,
550                    active: None,
551                };
552                program.steps.insert(at, step);
553                self.load(program)
554                    .map_err(|e| e.with_context(format!("add_step(id={id})")))?;
555                self.status()
556            }
557            ("edit_step", _) => {
558                let id = text("id")?;
559                let set = (args.get("args").cloned())
560                    .ok_or_else(|| GeopError::new("`args` (the arguments to set) is required"))?;
561                let mut program = self.editor.program().clone();
562                let step = program.step_mut(&id)?;
563                match (&mut step.args, set) {
564                    (Value::Object(args), Value::Object(set)) => args.extend(set),
565                    (args, set) => *args = set,
566                }
567                self.load(program)
568                    .map_err(|e| e.with_context(format!("edit_step(id={id})")))?;
569                self.status()
570            }
571            ("delete_step", _) => {
572                let id = text("id")?;
573                self.send(Command::Remove { id })?;
574                self.status()
575            }
576            ("run", _) => {
577                self.send(Command::Seek { marker: None })?;
578                self.status()
579            }
580            ("list_elements", _) => match args.get("placed").and_then(Value::as_str) {
581                Some(placed) => self.list_placed_elements(placed),
582                None => self.list_elements(),
583            },
584            ("inspect", _) => {
585                let inspector = text("inspector")?;
586                let elements = args.get("elements").cloned().unwrap_or(json!([]));
587                let options = args.get("options").cloned().unwrap_or(Value::Null);
588                self.inspect(&inspector, &elements, options)
589            }
590            ("export", Some(files)) => {
591                let path = text("path")?;
592                let format = match args.get("format").and_then(Value::as_str) {
593                    Some(format) => format.to_string(),
594                    None => (std::path::Path::new(&path).extension())
595                        .and_then(|e| e.to_str())
596                        .map(str::to_lowercase)
597                        .ok_or_else(|| GeopError::new(format!("{path} names no format")))?,
598                };
599                let options = args.get("options").cloned().unwrap_or(json!({}));
600                self.export(&path, &format, options, files)
601            }
602            ("save", Some(files)) => {
603                let path = text("path")?;
604                let program = self.editor.program().clone();
605                let json = program.to_json()?;
606                files.write(&path, json.as_bytes())?;
607                self.saved = program.state.clone();
608                if self.path.as_deref() != Some(path.as_str()) {
609                    self.send(Command::Load {
610                        program,
611                        path: Some(path.clone()),
612                    })?;
613                }
614                Ok(json!({ "saved": path, "bytes": json.len() }))
615            }
616            ("look", _) => {
617                let (png, camera) = self.picture(Some(args))?;
618                self.images.push(Image {
619                    media_type: "image/png",
620                    data: png,
621                });
622                Ok(camera)
623            }
624            ("undo", _) => {
625                self.send(Command::Undo)?;
626                self.editing()
627            }
628            ("redo", _) => {
629                self.send(Command::Redo)?;
630                self.editing()
631            }
632            ("open_step", _) => {
633                let placed = args
634                    .get("placed")
635                    .and_then(Value::as_str)
636                    .map(str::to_string);
637                let command = match (args.get("id").and_then(Value::as_str), placed) {
638                    (Some(id), placed) => Command::Open {
639                        id: id.to_string(),
640                        placed,
641                    },
642                    (None, None) => Command::New {
643                        kind: text("kind")?,
644                    },
645                    (None, Some(placed)) => Command::NewIn {
646                        kind: text("kind")?,
647                        placed,
648                    },
649                };
650                self.send(command)?;
651                self.editing()
652            }
653            ("step_event", _) => {
654                let event =
655                    (args.get("event")).ok_or_else(|| GeopError::new("`event` is required"))?;
656                for event in self.events(event)? {
657                    self.send(Command::Event { event })?;
658                }
659                self.editing()
660            }
661            ("commit_step", _) => {
662                self.open()?;
663                self.send(Command::Commit)?;
664                self.editing()
665            }
666            ("cancel_step", _) => {
667                self.open()?;
668                self.send(Command::Cancel)?;
669                self.editing()
670            }
671            ("take_tool", _) => {
672                let key = args.get("key").and_then(Value::as_str).map(str::to_string);
673                self.send(Command::Tool { key })?;
674                self.editing()
675            }
676            ("panel", _) => {
677                let key = text("key")?;
678                if let Some(payload) = args.get("payload") {
679                    self.send(Command::Panel {
680                        key: key.clone(),
681                        payload: payload.clone(),
682                    })?;
683                }
684                let panels = &self.shown()?.panels;
685                let shown = panels.get(&key).ok_or_else(|| {
686                    let keys: Vec<&str> = panels.keys().map(String::as_str).collect();
687                    GeopError::new(format!(
688                        "there is no panel `{key}`; there are {}",
689                        keys.join(", ")
690                    ))
691                })?;
692                Ok(json!({ "panel": key, "shows": shown, "status": self.status()? }))
693            }
694            _ => Err(GeopError::new(format!("there is no tool `{name}`"))),
695        }
696    }
697
698    /// The presentation shown: the open step's, or the tool's in hand.
699    fn presentation(&self) -> Option<&Presentation> {
700        (self.step.as_ref().map(|s| &s.presentation)).or(self.tool.as_ref())
701    }
702
703    /// The camera the next picture sees by — `view` (the tool `look`'s
704    /// input), if given — kept for the pixels of the events after it.
705    fn camera(&mut self, view: Option<&Value>) -> GeopResult<Camera> {
706        let vector = |key: &str| -> GeopResult<Option<Vector3>> {
707            let Some(value) = view.and_then(|v| v.get(key)) else {
708                return Ok(None);
709            };
710            let read: Option<Vec<f64>> =
711                (value.as_array()).map(|a| a.iter().filter_map(Value::as_f64).collect());
712            match read.as_deref() {
713                Some(&[x, y, z]) => Ok(Some(Vector3::from_array([x, y, z].map(Scalar::from_f64)))),
714                _ => Err(GeopError::new(format!("`{key}` is [x, y, z]: {value}"))),
715            }
716        };
717        let size = |key: &str, default: u32| -> GeopResult<u32> {
718            match view.and_then(|v| v.get(key)) {
719                None => Ok(default),
720                Some(value) => (value.as_u64())
721                    .filter(|&n| (16..=4096).contains(&n))
722                    .map(|n| n as u32)
723                    .ok_or_else(|| {
724                        GeopError::new(format!("`{key}` is 16 to 4096 pixels: {value}"))
725                    }),
726            }
727        };
728        let named = view.and_then(|v| v.get("view")).map(|name| {
729            (VIEWS.iter())
730                .find(|(n, ..)| Some(*n) == name.as_str())
731                .ok_or_else(|| GeopError::new(format!("there is no view {name}")))
732        });
733        let (look, up) = match (named.transpose()?, vector("look")?) {
734            (Some(_), Some(_)) => return Err(GeopError::new("`view` or `look`, not both")),
735            (Some((_, eye, up)), None) => (
736                Vector3::from_array(eye.map(|x| Scalar::from_f64(-x))),
737                Vector3::from_array(up.map(Scalar::from_f64)),
738            ),
739            (None, Some(look)) => (
740                look,
741                Vector3::from_array([0.0, 1.0, 0.0].map(Scalar::from_f64)),
742            ),
743            (None, None) => {
744                let resized =
745                    view.is_some_and(|v| v.get("width").is_some() || v.get("height").is_some());
746                if let Some(camera) = self.camera.clone().filter(|_| !resized) {
747                    return Ok(camera);
748                }
749                match self.presentation().and_then(|p| p.focus.as_ref()) {
750                    // Head on, from the side its normal points to: as the
751                    // editor turns the view to a plane worked in.
752                    Some(focus) => (focus.w().neg(), *focus.v()),
753                    None => {
754                        let (_, eye, up) = VIEWS[0];
755                        (
756                            Vector3::from_array(eye.map(|x| Scalar::from_f64(-x))),
757                            Vector3::from_array(up.map(Scalar::from_f64)),
758                        )
759                    }
760                }
761            }
762        };
763        let up = vector("up")?.unwrap_or(up);
764        let (width, height) = (size("width", PICTURE.0)?, size("height", PICTURE.1)?);
765        let camera = Camera::fitting(self.scene()?, look, up, width, height)?;
766        // A step drawn on a sheet of its own is framed to it.
767        let camera = match self.presentation().and_then(|p| p.sheet) {
768            Some(sheet) => Camera {
769                center: sheet.center,
770                span: sheet.size.to_f64(),
771                ..camera
772            },
773            None => camera,
774        };
775        if self.camera.as_ref() != Some(&camera) {
776            self.told = false;
777        }
778        self.camera = Some(camera.clone());
779        Ok(camera)
780    }
781
782    /// The picture `look` takes, told `view` (its input): a PNG, and the
783    /// camera it was seen by.
784    fn picture(&mut self, view: Option<&Value>) -> GeopResult<(Vec<u8>, Value)> {
785        let camera = self.camera(view)?;
786        self.tell_up()?;
787        let scene = self.scene()?;
788        let visuals = self.presentation().map(|p| &p.visuals[..]).unwrap_or(&[]);
789        let png = picture(scene, self.lit.as_ref(), visuals, &camera)?;
790        let vector = |v: &Vector3| v.to_array().map(|x| x.to_f64());
791        let origin = camera.pixel(&Vector3::from_array([0.0; 3].map(Scalar::from_f64)));
792        Ok((
793            png,
794            json!({
795                "camera": {
796                    "look": vector(&camera.look),
797                    "up": vector(&camera.up),
798                    "center": vector(&camera.center),
799                    "span": camera.span,
800                    "width": camera.width,
801                    "height": camera.height,
802                },
803                "origin_at_pixel": origin,
804                "pixels": "x from the left, y from the top; the picture shows `span` of the world from top to bottom",
805            }),
806        ))
807    }
808
809    /// Tells the step open which way is up in the camera, as the editor's
810    /// viewer does when its view comes to rest: a sketch's horizontal is
811    /// the picture's.
812    fn tell_up(&mut self) -> GeopResult<()> {
813        if self.told || self.step.is_none() {
814            return Ok(());
815        }
816        let Some(camera) = &self.camera else {
817            return Ok(());
818        };
819        let up = camera.up;
820        self.send(Command::Event {
821            event: StepEditEvent::View { up },
822        })?;
823        self.told = true;
824        Ok(())
825    }
826
827    /// The pointer an agent describes (see the tool `step_event`), in
828    /// `camera`.
829    fn pointer(camera: &Camera, spec: &Value) -> GeopResult<Pointer> {
830        let numbers = |key: &str| -> Option<Vec<f64>> {
831            let value = spec.get(key)?.as_array()?;
832            value.iter().map(Value::as_f64).collect()
833        };
834        let pointer = match (numbers("pixel").as_deref(), numbers("at").as_deref()) {
835            (Some(&[x, y]), None) => camera.pointer(x, y)?,
836            (None, Some(&[x, y, z])) => {
837                camera.through(Vector3::from_array([x, y, z].map(Scalar::from_f64)))?
838            }
839            _ => {
840                return Err(GeopError::new(format!(
841                    "a pointer is {{\"pixel\": [x, y]}} or {{\"at\": [x, y, z]}}: {spec}"
842                )));
843            }
844        };
845        let cycle = spec.get("cycle").and_then(Value::as_u64).unwrap_or(0);
846        Ok(pointer.cycled(cycle as usize))
847    }
848
849    /// Where `spec` (a pointer) is in `camera`'s picture.
850    fn pixel_of(camera: &Camera, spec: &Value) -> GeopResult<[f64; 2]> {
851        let pointer = Self::pointer(camera, spec)?;
852        Ok(camera.pixel(pointer.ray.origin()))
853    }
854
855    /// The editor's events of a pointer's event an agent describes: the
856    /// pointer moved there first, as a user's is, so that what it is over
857    /// is lit and grabbed as the editor would light and grab it.
858    fn pointer_events(&mut self, kind: &str, event: &Value) -> GeopResult<Vec<StepEditEvent>> {
859        let camera = self.camera(None)?;
860        self.tell_up()?;
861        let field = |key: &str| {
862            (event.get(key))
863                .ok_or_else(|| GeopError::new(format!("a `{kind}` event needs `{key}`")))
864        };
865        let shift = event.get("shift").and_then(Value::as_bool).unwrap_or(false);
866        let hover = |pointer: Pointer| StepEditEvent::Hover { pointer, shift };
867        Ok(match kind {
868            "hover" => vec![hover(Self::pointer(&camera, field("pointer")?)?)],
869            "click" => {
870                let pointer = Self::pointer(&camera, field("pointer")?)?;
871                let double = event
872                    .get("double")
873                    .and_then(Value::as_bool)
874                    .unwrap_or(false);
875                vec![
876                    hover(pointer),
877                    StepEditEvent::Click {
878                        pointer,
879                        double: false,
880                        shift,
881                    },
882                ]
883                .into_iter()
884                .chain(double.then_some(StepEditEvent::Click {
885                    pointer,
886                    double: true,
887                    shift,
888                }))
889                .collect()
890            }
891            "drag" => {
892                let (from, to) = (field("from")?, field("to")?);
893                let steps = event
894                    .get("steps")
895                    .and_then(Value::as_u64)
896                    .unwrap_or(8)
897                    .clamp(1, 100);
898                let start = Self::pointer(&camera, from)?;
899                let (a, b) = (Self::pixel_of(&camera, from)?, Self::pixel_of(&camera, to)?);
900                let mut events = vec![hover(start)];
901                for i in 1..=steps {
902                    let done = i == steps;
903                    let to = match done {
904                        true => Self::pointer(&camera, to)?,
905                        false => {
906                            let t = i as f64 / steps as f64;
907                            camera.pointer(a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t)?
908                        }
909                    };
910                    events.push(StepEditEvent::Drag {
911                        from: start,
912                        to,
913                        done,
914                        shift,
915                    });
916                }
917                events
918            }
919            "select_box" => {
920                let corner = |key: &str| -> GeopResult<[f64; 2]> {
921                    let value = field(key)?;
922                    match (value.as_array())
923                        .map(|a| a.iter().filter_map(Value::as_f64).collect::<Vec<_>>())
924                        .as_deref()
925                    {
926                        Some(&[x, y]) => Ok([x, y]),
927                        _ => Err(GeopError::new(format!(
928                            "a select_box's `{key}` is a pixel [x, y]: {value}"
929                        ))),
930                    }
931                };
932                let ([x0, y0], [x1, y1]) = (corner("from")?, corner("to")?);
933                let corners =
934                    [[x0, y0], [x1, y0], [x1, y1], [x0, y1]].map(|[x, y]| camera.pointer(x, y));
935                let [a, b, c, d] = corners;
936                vec![StepEditEvent::SelectBox {
937                    corners: Box::new([a?, b?, c?, d?]),
938                    crossing: x1 < x0,
939                    add: event.get("add").and_then(Value::as_bool).unwrap_or(false),
940                }]
941            }
942            _ => {
943                return Err(GeopError::new(format!(
944                    "there is no event `{kind}`: there are dialog, click, hover, drag, select_box, key and leave"
945                )));
946            }
947        })
948    }
949
950    /// Refuses with no step open.
951    fn open(&self) -> GeopResult<&StepState> {
952        (self.step.as_ref()).ok_or_else(|| GeopError::new("no step is open: open_step first"))
953    }
954
955    /// The step open, as the editor shows it — or, with none, the tool in
956    /// hand — and every step's status.
957    fn editing(&self) -> GeopResult<Value> {
958        let status = self.status()?;
959        let shown = |presentation: &Presentation| -> GeopResult<Value> {
960            let mut shown = serde_json::to_value(presentation)
961                .map_err(|e| GeopError::new(format!("showing the step: {e}")))?;
962            // What it draws is for a picture to show (`look`), and where
963            // its dialog goes, for a screen.
964            if let Value::Object(shown) = &mut shown {
965                shown.remove("visuals");
966                shown.remove("layout");
967            }
968            Ok(shown)
969        };
970        let Some(step) = &self.step else {
971            let program = self.shown()?;
972            return Ok(json!({
973                "open": null,
974                "tool": program.tool,
975                "tool_shows": self.tool.as_ref().map(shown).transpose()?,
976                "can_undo": program.can_undo,
977                "can_redo": program.can_redo,
978                "status": status,
979            }));
980        };
981        Ok(json!({
982            "open": {
983                "id": step.id,
984                "kind": step.kind,
985                "label": step.label,
986                "doc": step.doc,
987                "missing": step.missing,
988                "error": step.error,
989                "can_undo": step.can_undo,
990                "can_redo": step.can_redo,
991                "shows": shown(&step.presentation)?,
992            },
993            "status": status,
994        }))
995    }
996
997    /// The editor's events for the event an agent describes (see the tool
998    /// `step_event`).
999    fn events(&mut self, event: &Value) -> GeopResult<Vec<StepEditEvent>> {
1000        let kind = (event.get("type").and_then(Value::as_str))
1001            .ok_or_else(|| GeopError::new("an event needs `type`"))?;
1002        let field = |key: &str| {
1003            (event.get(key))
1004                .ok_or_else(|| GeopError::new(format!("a `{kind}` event needs `{key}`")))
1005        };
1006        Ok(match kind {
1007            "dialog" => {
1008                let key = (field("key")?.as_str())
1009                    .ok_or_else(|| GeopError::new("a dialog event's `key` is a string"))?;
1010                // A plain number or yes/no is what it looks like; anything
1011                // else is said as the editor's protocol says it.
1012                let value = match field("value")? {
1013                    Value::Number(n) => (n.as_f64().map(DialogValue::Number))
1014                        .ok_or_else(|| GeopError::new(format!("{n} is no number")))?,
1015                    Value::Bool(b) => DialogValue::Bool(*b),
1016                    value => serde_json::from_value(value.clone()).map_err(|e| {
1017                        GeopError::new(format!(
1018                            "a dialog value is a number, true or false, or {{\"type\": \"text\"|\"choice\"|\"press\"|..., \"value\": ...}}: {e}"
1019                        ))
1020                    })?,
1021                };
1022                vec![StepEditEvent::Dialog {
1023                    key: key.to_string(),
1024                    value,
1025                }]
1026            }
1027            "key" => {
1028                let key = (field("key")?.as_str())
1029                    .ok_or_else(|| GeopError::new("a key event's `key` is a string"))?;
1030                vec![StepEditEvent::Key {
1031                    key: key.to_string(),
1032                }]
1033            }
1034            "leave" => vec![StepEditEvent::Leave],
1035            _ => self.pointer_events(kind, event)?,
1036        })
1037    }
1038
1039    /// Replaces the program with `program`, in the file it is edited in.
1040    fn load(&mut self, program: Program) -> GeopResult<Update> {
1041        self.send(Command::Load {
1042            program,
1043            path: None,
1044        })
1045    }
1046
1047    /// Loads the program file `path`, with the other program files next to
1048    /// it, which the parts it places are read from.
1049    fn load_file(&mut self, path: &str, files: &mut dyn Files) -> GeopResult<Value> {
1050        let program = Program::from_json(&files.read(path)?)?;
1051        self.saved = program.state.clone();
1052        let beside = files.programs_beside(path);
1053        if !beside.is_empty() {
1054            self.send(Command::Files { files: beside })?;
1055        }
1056        self.send(Command::Load {
1057            program,
1058            path: Some(path.to_string()),
1059        })?;
1060        self.status()
1061    }
1062
1063    /// The program as last shown.
1064    fn shown(&self) -> GeopResult<&ProgramState> {
1065        (self.shown.as_ref()).ok_or_else(|| GeopError::new("the editor showed no program"))
1066    }
1067
1068    /// Every step, as the program's list shows it, and the part's solids.
1069    fn status(&self) -> GeopResult<Value> {
1070        let program = self.shown()?;
1071        let steps: Vec<Value> = (program.steps.iter())
1072            .map(|step| {
1073                json!({
1074                    "id": step.id,
1075                    "kind": step.kind,
1076                    "summary": step.summary,
1077                    "runs": step.runs,
1078                    "error": step.error,
1079                })
1080            })
1081            .collect();
1082        let failed = program.steps.iter().filter(|s| s.error.is_some()).count();
1083        let solids = self.scene.as_ref().map(|s| s.part.solids.clone());
1084        let tools: Vec<&str> = program.tools.iter().map(|t| t.key).collect();
1085        let panels: Vec<&str> = program.panels.keys().map(String::as_str).collect();
1086        Ok(json!({
1087            "steps": steps,
1088            "failed": failed,
1089            "solids": solids,
1090            "path": program.path,
1091            "open_step": self.step.as_ref().map(|s| json!({ "id": s.id, "kind": s.kind })),
1092            "moved": self.moved(),
1093            "tool": program.tool,
1094            "tools": tools,
1095            "panels": panels,
1096            "can_undo": program.can_undo,
1097            "can_redo": program.can_redo,
1098        }))
1099    }
1100
1101    /// The poses in the program's state that are not where they were
1102    /// loaded or saved, the furthest first: how far each went and turned.
1103    /// Lengths are read to [`MOVED`], which is only what is worth saying:
1104    /// it decides nothing else.
1105    fn moved(&self) -> Vec<Value> {
1106        let mut moved: Vec<(f64, f64, &str)> = (self.editor.program().state.iter())
1107            .filter_map(|(name, now)| {
1108                let (StateValue::Pose(now), Some(StateValue::Pose(saved))) =
1109                    (now, self.saved.get(name))
1110                else {
1111                    return None;
1112                };
1113                let relative = saved.inverse().compose(now);
1114                let by = relative.position().to_array().map(|x| x.to_f64());
1115                let by = by.iter().map(|x| x * x).sum::<f64>().sqrt();
1116                let turn = relative.rotation();
1117                let across = [turn.x, turn.y, turn.z].map(|x| x.to_f64());
1118                let across = across.iter().map(|x| x * x).sum::<f64>().sqrt();
1119                let turned = 2.0 * across.atan2(turn.w.to_f64().abs());
1120                (by > MOVED || turned > MOVED).then_some((by, turned.to_degrees(), name.as_str()))
1121            })
1122            .collect();
1123        moved.sort_by(|a, b| b.0.total_cmp(&a.0).then(b.1.total_cmp(&a.1)));
1124        (moved.into_iter())
1125            .map(|(by, turned, name)| json!({ "state": name, "moved": by, "turned_degrees": turned }))
1126            .collect()
1127    }
1128
1129    /// Every operation, by kind, label, group and what it does; or the one
1130    /// of `kind` alone, with the arguments a new step of it starts with and
1131    /// the form it is edited through.
1132    fn list_operations(&mut self, kind: Option<&str>) -> GeopResult<Value> {
1133        let operations = self.shown()?.operations.clone();
1134        let Some(kind) = kind else {
1135            let operations: Vec<Value> = (operations.iter())
1136                .map(|listed| {
1137                    json!({
1138                        "kind": listed.kind.to_string(),
1139                        "label": listed.info.label,
1140                        "group": listed.info.group,
1141                        "doc": listed.info.doc,
1142                        "experimental": listed.info.experimental,
1143                    })
1144                })
1145                .collect();
1146            return Ok(json!({ "operations": operations }));
1147        };
1148        let listed = (operations.iter())
1149            .find(|listed| listed.kind == kind)
1150            .ok_or_else(|| {
1151                let kinds: Vec<String> = (operations.iter())
1152                    .map(|listed| listed.kind.to_string())
1153                    .collect();
1154                GeopError::new(format!(
1155                    "there is no operation `{kind}`; there are {}",
1156                    kinds.join(", ")
1157                ))
1158            })?;
1159        let opened = self
1160            .send(Command::New {
1161                kind: kind.to_string(),
1162            })
1163            .map_err(|e| e.with_context(format!("opening a new `{kind}`")))?;
1164        let step = (opened.step)
1165            .ok_or_else(|| GeopError::new(format!("a new `{kind}` opened no step")))?;
1166        let default_args = self.editor.editing().map(|s| s.args.clone());
1167        // Cancelling what was just opened cannot be refused.
1168        let _ = self.send(Command::Cancel);
1169        let form = serde_json::to_value(&step.presentation.dialog)
1170            .map_err(|e| GeopError::new(format!("the form of `{kind}`: {e}")))?;
1171        Ok(json!({
1172            "kind": kind,
1173            "label": listed.info.label,
1174            "group": listed.info.group,
1175            "tier": listed.info.tier,
1176            "doc": if step.doc.is_empty() { listed.info.doc } else { step.doc },
1177            "experimental": listed.info.experimental,
1178            "default_args": default_args,
1179            "missing": step.missing,
1180            "form": form,
1181        }))
1182    }
1183
1184    /// The scene as last drawn.
1185    fn scene(&self) -> GeopResult<&SceneState> {
1186        self.scene
1187            .as_ref()
1188            .ok_or_else(|| GeopError::new("nothing has been drawn yet: start with new_part"))
1189    }
1190
1191    fn list_elements(&self) -> GeopResult<Value> {
1192        let scene = self.scene()?;
1193        let mut listed = Self::elements_of(&scene.part);
1194        let structure: Vec<Value> = (scene.structure.iter().flatten())
1195            .map(|item| json!({ "kind": item.kind, "name": item.name }))
1196            .collect();
1197        listed.insert("structure".into(), json!(structure));
1198        Ok(Value::Object(listed))
1199    }
1200
1201    /// The elements of the part placed at `placed`, in its own frame, and
1202    /// how to refer to them from the part it is placed in.
1203    fn list_placed_elements(&self, placed: &str) -> GeopResult<Value> {
1204        let part = &self.scene()?.part;
1205        let instance = (part.instances.iter())
1206            .find(|instance| instance.name == placed)
1207            .ok_or_else(|| {
1208                let names: Vec<&str> = part.instances.iter().map(|i| i.name.as_str()).collect();
1209                GeopError::new(format!(
1210                    "no part is placed as `{placed}`; there are {}",
1211                    names.join(", ")
1212                ))
1213            })?;
1214        let view = &instance.view;
1215        let vector = |v: &Vector3| v.to_array().map(|x| x.to_f64());
1216        let mut listed = Self::elements_of(view);
1217        listed.insert("placed".into(), json!(placed));
1218        let target = Located::in_instance(placed, &view.model).key();
1219        listed.insert("target".into(), json!(target));
1220        listed.insert(
1221            "frame".into(),
1222            json!({
1223                "origin": vector(instance.frame.origin()),
1224                "x": vector(instance.frame.u()),
1225                "y": vector(instance.frame.v()),
1226                "z": vector(instance.frame.w()),
1227            }),
1228        );
1229        listed.insert(
1230            "extent".into(),
1231            json!({
1232                "center": vector(&view.extent.center),
1233                "size": view.extent.size.to_f64(),
1234            }),
1235        );
1236        Ok(Value::Object(listed))
1237    }
1238
1239    /// The elements of `part`, by name.
1240    fn elements_of(part: &PartView) -> serde_json::Map<String, Value> {
1241        let faces: Vec<Value> = (part.faces.iter())
1242            .map(|f| json!({ "name": f.name, "solid": f.solid, "feature": f.feature }))
1243            .collect();
1244        let edges: Vec<Value> = (part.edges.iter())
1245            .map(|e| json!({ "name": e.name, "solid": e.solid, "faces": e.faces }))
1246            .collect();
1247        let vertices: Vec<Value> = (part.vertices.iter())
1248            .map(|v| {
1249                let at = v.at.to_array().map(|c| c.to_f64());
1250                json!({ "name": v.name, "solid": v.solid, "at": at })
1251            })
1252            .collect();
1253        let sketches: Vec<&str> = part.sketches.iter().map(|s| s.name.as_str()).collect();
1254        let datums: Vec<Value> = (part.datums.iter())
1255            .map(|d| json!({ "name": d.name, "kind": d.kind }))
1256            .collect();
1257        // Every part placed, not only those the last scene sent anew.
1258        let placed: Vec<&str> = part.instances.iter().map(|i| i.name.as_str()).collect();
1259        [
1260            ("solids", json!(part.solids)),
1261            ("faces", json!(faces)),
1262            ("edges", json!(edges)),
1263            ("vertices", json!(vertices)),
1264            ("sketches", json!(sketches)),
1265            ("datums", json!(datums)),
1266            ("placed_parts", json!(placed)),
1267        ]
1268        .into_iter()
1269        .map(|(key, value)| (key.to_string(), value))
1270        .collect()
1271    }
1272
1273    /// Asks `inspector` about the part — of `elements`, for one that picks.
1274    fn inspect(&mut self, inspector: &str, elements: &Value, options: Value) -> GeopResult<Value> {
1275        let elements = (elements.as_array())
1276            .ok_or_else(|| GeopError::new("`elements` (a list of {kind, name}) is required"))?;
1277        let picked: Vec<PickedElement> = (elements.iter())
1278            .map(|e| {
1279                let field = |key: &str| {
1280                    e.get(key)
1281                        .and_then(Value::as_str)
1282                        .map(str::to_string)
1283                        .ok_or_else(|| GeopError::new(format!("an element needs `{key}`: {e}")))
1284                };
1285                Ok(PickedElement {
1286                    target: Located::new(self.scene()?.part.model.clone()),
1287                    kind: field("kind")?,
1288                    name: field("name")?,
1289                })
1290            })
1291            .collect::<GeopResult<_>>()?;
1292        let update = self.send(Command::Inspect {
1293            inspector: inspector.to_string(),
1294            options,
1295            picked,
1296        })?;
1297        let inspection = (update.inspection)
1298            .ok_or_else(|| GeopError::new(format!("`{inspector}` answered nothing")))?;
1299        Ok(inspection.report)
1300    }
1301
1302    /// Writes the part to `path` with the exporter of `format`.
1303    fn export(
1304        &mut self,
1305        path: &str,
1306        format: &str,
1307        options: Value,
1308        files: &mut dyn Files,
1309    ) -> GeopResult<Value> {
1310        let outputs = self.editor.registry().outputs();
1311        let exporter = outputs.exporter_for(format)?.info().kind.to_string();
1312        let update = self.send(Command::Export {
1313            exporter: exporter.clone(),
1314            format: Some(format.to_string()),
1315            options,
1316            date: files.today(),
1317        })?;
1318        let export = (update.export)
1319            .ok_or_else(|| GeopError::new(format!("the exporter `{exporter}` wrote nothing")))?;
1320        files.write(path, export.bytes())?;
1321        Ok(json!({
1322            "written": path,
1323            "exporter": exporter,
1324            "format": format,
1325            "bytes": export.bytes().len(),
1326            "media_type": export.media_type,
1327        }))
1328    }
1329}