1use 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#[derive(Clone, Debug, PartialEq)]
55pub struct AgentTool {
56 pub name: &'static str,
57 pub description: &'static str,
58 pub input_schema: Value,
59 pub files: bool,
63}
64
65impl AgentTool {
66 pub fn mcp(&self) -> Value {
68 json!({ "name": self.name, "description": self.description, "inputSchema": self.input_schema })
69 }
70
71 pub fn anthropic(&self) -> Value {
73 json!({ "name": self.name, "description": self.description, "input_schema": self.input_schema })
74 }
75}
76
77pub 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
236const 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
249const MOVED: f64 = 1e-6;
252
253const PICTURE: (u32, u32) = (800, 600);
255
256const 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
267pub trait Files {
270 fn read(&mut self, path: &str) -> GeopResult<String>;
272 fn programs_beside(&mut self, path: &str) -> BTreeMap<String, Option<String>>;
276 fn write(&mut self, path: &str, bytes: &[u8]) -> GeopResult<()>;
278 fn today(&self) -> String;
280}
281
282#[derive(Clone, Debug, Serialize)]
286pub struct ToolAnswer {
287 pub text: String,
288 pub is_error: bool,
289 pub updates: Vec<Update>,
290 pub images: Vec<Image>,
292}
293
294#[derive(Clone, Debug, PartialEq, Serialize)]
296pub struct Image {
297 pub media_type: &'static str,
298 #[serde(serialize_with = "as_base64")]
300 pub data: Vec<u8>,
301}
302
303impl Image {
304 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
316pub struct Session {
319 editor: Editor,
320 scene: Option<SceneState>,
322 shown: Option<ProgramState>,
324 step: Option<StepState>,
326 tool: Option<Presentation>,
328 lit: Option<Lighting>,
330 program: Program,
333 path: Option<String>,
335 sent: Vec<Update>,
337 images: Vec<Image>,
339 camera: Option<Camera>,
342 told: bool,
344 saved: StateValues,
347}
348
349impl Session {
350 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 session.editor.forget_viewer();
371 session
372 }
373
374 pub fn editor(&self) -> &Editor {
376 &self.editor
377 }
378
379 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 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 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 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 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 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 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 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 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 fn presentation(&self) -> Option<&Presentation> {
700 (self.step.as_ref().map(|s| &s.presentation)).or(self.tool.as_ref())
701 }
702
703 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 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 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 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 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 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 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 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 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 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 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 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 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 fn load(&mut self, program: Program) -> GeopResult<Update> {
1041 self.send(Command::Load {
1042 program,
1043 path: None,
1044 })
1045 }
1046
1047 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 fn shown(&self) -> GeopResult<&ProgramState> {
1065 (self.shown.as_ref()).ok_or_else(|| GeopError::new("the editor showed no program"))
1066 }
1067
1068 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 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 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 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 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 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 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 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 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 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}