package tiny_appkits

  1. Overview
  2. Docs
Application engines from scratch: a spreadsheet, rich text, paint, draw, CAD, editors and more

Install

dune-project
 Dependency

Authors

Maintainers

Sources

0.3.6.tar.gz
md5=7c636383d146d30ac6f2fa234a6253c8
sha512=c79f3823c5f8f57e5038eb640d487c61168b84aa07c61999d6622ef9fd0c890e2b03b4c6a7cdbbe9352a49e25dda00ac7bb14693cee8e3d7beeed251351a2af0

doc/src/tiny_appkits.appkit_diagram/Diagram.ml.html

Source file Diagram.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
(* Claude Code
 *
 * Copyright (C) 2026 Yoann Padioleau
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public License
 * (LGPL) as published by the Free Software Foundation; either version
 * 2 of the License, or (at your option) any later version.
 *)

(* See Diagram.mli *)

type kind = Box | Connector
type ends = Begin | End

type master = {
  name : string;
  kind : kind;
  cells : (string * string) list;
  geometry : (string * string) list;
  points : (string * string * Ortho_route.dir) list;
}

type shape = {
  id : int;
  master : string;
  kind : kind;
  rows : (string * string) list;
  geometry : (string * string) list;
  points : (string * string * Ortho_route.dir) list;
  text : string;
  glue : (ends * int * int) list;
}

type t = { sheet : Sheet.t; shapes : shape list; next : int }

let empty = { sheet = Sheet.empty; shapes = []; next = 1 }
let shapes t = t.shapes
let shape t id = List.find_opt (fun s -> s.id = id) t.shapes

(*****************************************************************************)
(* The stencil *)
(*****************************************************************************)

(* a box's master: its size, cells of its own (User.), its outline as
   formulas, and a connection point in the middle of each side *)
let box name ?(user = []) (outline : (string * string) list) : master =
  let geometry = List.mapi (fun i _ -> (Printf.sprintf "Geometry1.X%d" (i + 1), Printf.sprintf "Geometry1.Y%d" (i + 1))) outline in
  let sides = [ ("Width*0.5", "Height", Ortho_route.Up); ("Width", "Height*0.5", Ortho_route.Right); ("Width*0.5", "0", Ortho_route.Down); ("0", "Height*0.5", Ortho_route.Left) ] in
  let points = List.mapi (fun i (_, _, d) -> (Printf.sprintf "Connections.X%d" (i + 1), Printf.sprintf "Connections.Y%d" (i + 1), d)) sides in
  let cells =
    [ ("Width", "1.5"); ("Height", "0.75") ]
    @ user
    @ List.concat (List.map2 (fun (xn, yn) (x, y) -> [ (xn, x); (yn, y) ]) geometry outline)
    @ List.concat (List.map2 (fun (xn, yn, _) (x, y, _) -> [ (xn, x); (yn, y) ]) points sides)
  in
  { name; kind = Box; cells; geometry; points }

let masters =
  [
    box "Process" [ ("0", "0"); ("Width", "0"); ("Width", "Height"); ("0", "Height") ];
    box "Decision" [ ("Width*0.5", "0"); ("Width", "Height*0.5"); ("Width*0.5", "Height"); ("0", "Height*0.5") ];
    (* the slant stays the same however wide the shape *)
    box "Data" ~user:[ ("User.Slant", "MIN(0.25, Width*0.2)") ]
      [ ("User.Slant", "0"); ("Width", "0"); ("Width-User.Slant", "Height"); ("0", "Height") ];
    box "Preparation" ~user:[ ("User.Cut", "MIN(Height*0.5, Width*0.25)") ]
      [ ("User.Cut", "0"); ("Width-User.Cut", "0"); ("Width", "Height*0.5"); ("Width-User.Cut", "Height"); ("User.Cut", "Height"); ("0", "Height*0.5") ];
    (* the ShapeSheet's classic: the head keeps its length *)
    box "Block arrow" ~user:[ ("User.Head", "MIN(0.5, Width*0.5)"); ("User.Shaft", "Height*0.25") ]
      [
        ("0", "Height*0.5-User.Shaft"); ("Width-User.Head", "Height*0.5-User.Shaft"); ("Width-User.Head", "0"); ("Width", "Height*0.5");
        ("Width-User.Head", "Height"); ("Width-User.Head", "Height*0.5+User.Shaft"); ("0", "Height*0.5+User.Shaft");
      ];
    { name = "Dynamic connector"; kind = Connector; cells = []; geometry = []; points = [] };
  ]

(*****************************************************************************)
(* Names into cells *)
(*****************************************************************************)

let row_of (s : shape) name =
  let rec go i = function [] -> None | (n, _) :: rest -> if n = name then Some i else go (i + 1) rest in
  go 0 s.rows

let is_start c = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
let is_name c = is_start c || (c >= '0' && c <= '9') || c = '.' || c = '_'

(* a formula with names into the engine's, with cells: Width into C3,
   Sheet.2!PinX into B1; a function's name kept *)
let translate (t : t) (self : shape) (text : string) : (string, string) result =
  match float_of_string_opt (String.trim text) with
  | Some _ -> Ok (String.trim text)
  | None -> (
      let n = String.length text in
      let buf = Buffer.create (n + 8) in
      let word i = let j = ref i in while !j < n && is_name text.[!j] do incr j done; !j in
      let cell (s : shape) name =
        match row_of s name with Some r -> Ok (Formula.name_of_cell (s.id, r)) | None -> Error ("no cell " ^ name)
      in
      let rec go i =
        if i >= n then Ok ()
        else if is_start text.[i] then
          let j = word i in
          let name = String.sub text i (j - i) in
          if j < n && text.[j] = '!' then
            let k = word (j + 1) in
            let other = String.sub text (j + 1) (k - j - 1) in
            let target = match int_of_string_opt (String.sub name 6 (max 0 (String.length name - 6))) with Some id when String.length name > 6 && String.sub name 0 6 = "Sheet." -> shape t id | _ -> None in
            match target with
            | None -> Error ("no shape " ^ name)
            | Some s -> ( match cell s other with Ok c -> Buffer.add_string buf c; go k | Error e -> Error e)
          else if j < n && text.[j] = '(' then begin Buffer.add_string buf (String.uppercase_ascii name); go j end
          else match cell self name with Ok c -> Buffer.add_string buf c; go j | Error e -> Error e
        else begin Buffer.add_char buf text.[i]; go (i + 1) end
      in
      match go 0 with
      | Ok () ->
          let engine = Buffer.contents buf in
          (match Formula.parse engine with Ok _ -> Ok ("=" ^ engine) | Error e -> Error e)
      | Error e -> Error e)

let replace_shape t (s : shape) = { t with shapes = List.map (fun x -> if x.id = s.id then s else x) t.shapes }

let set_formula t id name text =
  match shape t id with
  | None -> Error "no such shape"
  | Some s -> (
      match row_of s name with
      | None -> Error ("no cell " ^ name)
      | Some r -> (
          match translate t s text with
          | Error e -> Error e
          | Ok engine ->
              let s = { s with rows = List.map (fun (n, f) -> if n = name then (n, String.trim text) else (n, f)) s.rows } in
              Ok { (replace_shape t s) with sheet = Sheet.set (id, r) engine t.sheet }))

let value t id name =
  match Option.bind (shape t id) (fun s -> row_of s name) with
  | Some r -> ( match Sheet.value t.sheet (id, r) with Sheet.Number f -> Some f | _ -> None)
  | None -> None

let shown t id name = match Option.bind (shape t id) (fun s -> row_of s name) with Some r -> Sheet.show (Sheet.value t.sheet (id, r)) | None -> ""
let num t id name = Option.value (value t id name) ~default:0.
let number f = Printf.sprintf "%g" (Float.round (f *. 10000.) /. 10000.)

(* a cell set, or left as it was when the formula is refused *)
let set t id name text = match set_formula t id name text with Ok t -> t | Error _ -> t

(*****************************************************************************)
(* Shapes *)
(*****************************************************************************)

let drop t (m : master) (x, y) =
  let id = t.next in
  let cells =
    match m.kind with
    | Box -> [ ("PinX", number x); ("PinY", number y) ] @ m.cells
    | Connector -> [ ("BeginX", number (x -. 0.75)); ("BeginY", number y); ("EndX", number (x +. 0.75)); ("EndY", number y) ]
  in
  (* every row named first, so that a formula can name a row below it *)
  let s = { id; master = m.name; kind = m.kind; rows = List.map (fun (n, _) -> (n, "")) cells; geometry = m.geometry; points = m.points; text = ""; glue = [] } in
  let t = { t with shapes = t.shapes @ [ s ]; next = id + 1 } in
  (List.fold_left (fun t (n, f) -> set t id n f) t cells, id)

let set_text t id text = match shape t id with Some s -> replace_shape t { s with text } | None -> t
let move t id (x, y) = set (set t id "PinX" (number x)) id "PinY" (number y)

let resize t id (w, h) =
  let left = num t id "PinX" -. (num t id "Width" /. 2.) and top = num t id "PinY" +. (num t id "Height" /. 2.) in
  let w = Float.max 0.1 w and h = Float.max 0.1 h in
  let t = set (set t id "Width" (number w)) id "Height" (number h) in
  move t id (left +. (w /. 2.), top -. (h /. 2.))

let end_cells = function Begin -> ("BeginX", "BeginY") | End -> ("EndX", "EndY")

let unglue t id e = match shape t id with Some s -> replace_shape t { s with glue = List.filter (fun (e', _, _) -> e' <> e) s.glue } | None -> t

let glue t id e ~target ~point =
  match (shape t id, shape t target) with
  | Some _, Some s when point < List.length s.points ->
      let xn, yn, _ = List.nth s.points point in
      let x, y = end_cells e in
      let sheet = Printf.sprintf "Sheet.%d!" target in
      let t = set t id x (Printf.sprintf "%sPinX-%sWidth*0.5+%s%s" sheet sheet sheet xn) in
      let t = set t id y (Printf.sprintf "%sPinY-%sHeight*0.5+%s%s" sheet sheet sheet yn) in
      let t = unglue t id e in
      (match shape t id with Some c -> replace_shape t { c with glue = (e, target, point) :: c.glue } | None -> t)
  | _ -> t

let place_end t id e (px, py) =
  let x, y = end_cells e in
  unglue (set (set t id x (number px)) id y (number py)) id e

let delete t id =
  let mention = Printf.sprintf "Sheet.%d!" id in
  let contains s sub = let n = String.length sub in let rec go i = i + n <= String.length s && (String.sub s i n = sub || go (i + 1)) in go 0 in
  (* what names the shape keeps its value and loses its formula *)
  let t =
    List.fold_left
      (fun t (s : shape) ->
        if s.id = id then t
        else
          let t = List.fold_left (fun t (n, f) -> if contains f mention then set t s.id n (number (num t s.id n)) else t) t s.rows in
          match shape t s.id with Some s -> replace_shape t { s with glue = List.filter (fun (_, target, _) -> target <> id) s.glue } | None -> t)
      t t.shapes
  in
  match shape t id with
  | None -> t
  | Some s ->
      let sheet = List.fold_left (fun sheet r -> Sheet.set (id, r) "" sheet) t.sheet (List.init (List.length s.rows) Fun.id) in
      { t with sheet; shapes = List.filter (fun x -> x.id <> id) t.shapes }

(*****************************************************************************)
(* On the page *)
(*****************************************************************************)

let corner t (s : shape) = (num t s.id "PinX" -. (num t s.id "Width" /. 2.), num t s.id "PinY" -. (num t s.id "Height" /. 2.))
let outline t (s : shape) = let x0, y0 = corner t s in List.map (fun (xn, yn) -> (x0 +. num t s.id xn, y0 +. num t s.id yn)) s.geometry

let connection_points t (s : shape) =
  let x0, y0 = corner t s in
  List.map (fun (xn, yn, d) -> ((x0 +. num t s.id xn, y0 +. num t s.id yn), d)) s.points

let bounds t (s : shape) =
  let x0, y0 = corner t s in
  (x0, y0, x0 +. num t s.id "Width", y0 +. num t s.id "Height")

let ends_of t (s : shape) = ((num t s.id "BeginX", num t s.id "BeginY"), (num t s.id "EndX", num t s.id "EndY"))

let route t (s : shape) =
  let b, e = ends_of t s in
  let side which =
    match List.find_opt (fun (e', _, _) -> e' = which) s.glue with
    | Some (_, target, point) -> Option.bind (shape t target) (fun x -> Option.map (fun (_, _, d) -> d) (List.nth_opt x.points point))
    | None -> None
  in
  let boxes = List.filter_map (fun (x : shape) -> if x.kind = Box then Some (bounds t x) else None) t.shapes in
  Ortho_route.route ~boxes ~margin:0.15 (b, side Begin) (e, side End)