← Indice Come è fatto il visore 3D

Come è fatto il visore 3D

Il visore 3D è la pagina dove la parete si gira, si apre e si taglia (c'è anche quello della nostra idea, Visore 3D - Idea, stessa pagina con altri modelli). Fino a questa versione era una pagina HTML con circa 600 righe di JavaScript scritte a mano; ora la logica è scritta in Nim, compilata in JavaScript, e la pagina è un documento nimib con un solo blocco nbJsFromCode (il blocco di nimib che compila Nim per il browser).

Questa pagina spiega come funziona il visore e come è avvenuto il passaggio.

Perché

Prima il percorso dei dati era questo:

Nim (C)                      testo                     JavaScript
parete(p) ─► Model ─► toJson ─► 12 modelli JSON ─► visore3d.tpl.html (600 righe)
                              incollati nel template     campi rinominati: w, d, h,
                                                         holesTop.y = holesPiano.z ...

Il visore non vedeva il tipo Part di Caderò ma una sua copia in JSON, con nomi diversi: w, d, h invece di size.x, size.y, size.z; nei fori del piano y e d invece di z e h. Il pannello frontale riceveva come fori anche le sagome dei frontali, ma solo nel JSON. Ogni nuova proprietà delle parti (apre, alzata, dentro...) andava aggiunta in tre posti: il tipo in Nim, la conversione in JSON, il JavaScript. E il JavaScript non aveva nessun controllo: un nome sbagliato diventava undefined senza errori.

Ora:

Nim (JS, nel browser)
parete(p) ─► Model ─► avviaVisore ─► three.js

I pezzi

file cosa contiene
src/cadero/js/three.nim i binding: le classi e i metodi di three.js che servono, visti da Nim
src/cadero/js/pagina.nim le poche cose del browser che mancano in std/dom
src/cadero/visore.nim il visore: scena, tocchi, esploso, in Nim
docsrc/visore3d.nim, docsrc/visore3d_idea.nim le due pagine (falegnami e idea): modelli e testi, il blocco nbJsFromCode
docsrc/pagina_visore.nim il renderer comune: template, carta da parati
tools/visore3d.tpl.html stile, pulsanti, posti per la tavola e per lo script
tools/visore3d.nim dalla pagina di Pages all'artefatto claude.ai

1. Il blocco nbJsFromCode

Il cuore della pagina è questo blocco. Non gira quando si genera il documento: nimib lo scrive in un file, lo compila con nim js e mette il JavaScript che esce nella pagina, dentro un <script>. Le variabili tra parentesi (qui cartaSrc, l'immagine della carta da parati) sono catturate: nimib le trasforma in JSON durante la generazione e il blocco le ritrova nel browser.

docsrc/visore3d_idea.nim

nbJsFromCode(cartaSrc):
  import ../src/cadero/visore, ../models/parete

  # nel visore tutto parte chiuso: cassetti, ante, ribalte e tavolino si aprono
  # toccandoli. Tutte le combinazioni delle opzioni, con la chiave che usa il
  # visore: "base" o "tavolino", poi "Scoperta" (risega), "Antina" (solo con il
  # tavolino), "Comodino" (prese sul comodino); es. "tavolinoScopertaComodino".
  # I modelli si calcolano qui, nel browser, con lo stesso codice Nim delle tavole.
  var varianti: Table[string, Model]
  for tav in [false, true]:
    for scoperta in [false, true]:
      for antina in (if tav: @[false, true] else: @[false]):
        for comodino in [false, true]:
          var p = rilievo()
          p.disegno.cassettoAperto = -1
          if tav: p = p.conTavolino(aperto = false)
          p.libreria.risegaRivestita = not scoperta
          p.tavolino.antina = antina
          p.prese.sulComodino = comodino
          # (la chiave si scrive a pezzi: nimib ricopia il blocco come testo e
          # un `(if ...) & (if ...)` su più righe non sopravvive alla copia)
          var chiave = "base"
          if tav: chiave = "tavolino"
          if scoperta: chiave.add "Scoperta"
          if antina: chiave.add "Antina"
          if comodino: chiave.add "Comodino"
          varianti[chiave] = parete(p)
  let carta = Carta(src: cartaSrc, w: 106, h: 52.6)
  quandoThreePronto(proc () = avviaVisore(varianti, carta))

Le varianti sono le stesse di prima (base o con tavolino, risega rivestita o scoperta, antina, prese sul comodino: 12 combinazioni), ma ora si calcolano nel browser invece di arrivare come 12 modelli JSON incollati nella pagina. Il file da compilare nimib lo scrive nella cartella dei documenti (docs/): per questo gli import cominciano con ../.

2. three.js visto da Nim

three.js è una libreria JavaScript. Per usarla da Nim si scrivono dei binding: dichiarazioni Nim che dicono al compilatore quale JavaScript scrivere. Non serve un binding completo, solo le classi e i metodi che il visore usa (circa 80). Due esempi dal file dei binding:

src/cadero/js/three.nim

  Vector3* = ref object of JsRoot
    x*, y*, z*: float
  Euler* = ref object of JsRoot
    x*, y*, z*: float
  Color* = ref object of JsRoot
  Box3* = ref object of JsRoot
    min*, max*: Vector3

src/cadero/js/three.nim

proc newBox3*(): Box3 {.importjs: "new THREE.Box3()".}
proc setFromObject*(b: Box3; o: Object3D): Box3 {.importjs: "#.setFromObject(@)", discardable.}
proc expandByObject*(b: Box3; o: Object3D): Box3 {.importjs: "#.expandByObject(@)", discardable.}
proc getCenter*(b: Box3; v: Vector3): Vector3 {.importjs: "#.getCenter(@)", discardable.}
proc getSize*(b: Box3; v: Vector3): Vector3 {.importjs: "#.getSize(@)", discardable.}

I binding hanno i nomi di three.js, così il codice del visore si legge con la documentazione di three.js accanto.

Come arriva three.js

three.js è copiato nel repository (vendor/three/, la versione 0.160.0) e la generazione dei documenti lo mette in docs/three/, accanto alle pagine: nessuna chiamata a un CDN. Si carica come modulo ES, con un importmap. Un modulo parte dopo gli script normali, e quello generato da Nim è uno script normale. Allora un piccolo modulo nella pagina mette three.js in window.THREE e manda un evento; il codice Nim aspetta quell'evento con quandoThreePronto:

tools/visore3d.tpl.html

<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
window.THREE = { ...THREE, OrbitControls };
window.dispatchEvent(new Event('three-pronto'));
</script>

src/cadero/js/three.nim

proc quandoThreePronto*(avvia: proc ()) =
  ## three.js è un modulo ES: si carica dopo gli script normali (come quello
  ## generato da Nim). La pagina lo mette in `window.THREE` e manda l'evento
  ## "three-pronto"; se è già arrivato, si parte subito.
  proc giaPronto(): bool {.importjs: "(typeof window.THREE === 'object')".}
  proc aspetta(f: proc ()) {.importjs: "window.addEventListener('three-pronto', #, {once: true})".}
  if giaPronto(): avvia() else: aspetta(avvia)

3. Dalla parte alla scena

costruisci(m) scorre le parti del modello. Per ognuna sceglie la forma, la mette nella scena con il suo materiale e i suoi spigoli e, se è apribile, la aggancia al suo gruppo. Gli assi cambiano nome: in three.js l'alto è y e la profondità va verso −z, quindi (x, y, z) del CAD diventano (x, z, −y).

src/cadero/visore.nim

proc geometria(p: Part): BufferGeometry =
  ## La forma di una parte: cilindro (foro maniglia), sagoma estrusa o scatola.
  if p.kind == kKnob:
    result = newCylinderGeometry(p.w / 2, p.w / 2, 0.4, 20)
    result.rotateX(PI / 2)
    result.translate(p.x, p.z, -p.y + 0.15)
  elif p.holesPiano.len > 0 or p.taccheP.len > 0:
    result = extrudedTop(p)
  elif p.holes.len > 0 or p.contorno.len > 0 or (p.kind in {kFront, kBox} and p.r > 0):
    result = extruded(p)
  else:
    result = newBoxGeometry(p.w, p.h, p.d)
    result.translate(p.x + p.w / 2, p.z + p.h / 2, -(p.y + p.d / 2))

Per leggere il codice come prima, la parte ha anche i nomi brevi di una volta, calcolati dai campi veri:

src/cadero/visore.nim

proc x(p: Part): float = p.pos.x
proc y(p: Part): float = p.pos.y
proc z(p: Part): float = p.pos.z
proc w(p: Part): float = p.size.x
proc d(p: Part): float = p.size.y
proc h(p: Part): float = p.size.z

Il pannello frontale nel 3D ha come fori anche le sagome di ante e cassetti, che vanno a occuparli. Prima questo succedeva solo nella conversione in JSON; ora è una funzione del modello, usata sia dal visore sia da toJson:

src/cadero/model.nim

proc foriVisti*(m: Model; p: Part): seq[RRect] =
  ## Le aperture di una parte come le disegna il 3D. Il pannello frontale
  ## (un pannello con aperture) riceve come fori anche le sagome dei frontali
  ## e dei cassetti, che vanno a occuparle; le altre parti tengono le loro.
  if p.kind == kPanel and p.holes.len > 0 and p.name == "fronte":
    result = p.holes
    for q in m.parts:
      if q.kind in {kFront, kBox}: result.add q.frontRect
  else: result = p.holes

4. Aprire e chiudere

Ogni gruppo apribile (Unita) ha due contenitori: pivot, che sta sullo spigolo della cerniera e ruota o trasla, e inner, che riporta le parti al loro posto. k va da 0 (chiuso) a 1 (aperto) e a ogni fotogramma insegue target; posa dice dove sta il gruppo per un certo k, secondo il suo modo di aprirsi (apre):

src/cadero/visore.nim

proc posa(u: Unita) =
  ## Muove un gruppo: k va da 0 (chiuso) a 1 (aperto).
  let k = u.k
  case u.apre
  of "tira": u.pivot.position.z = (if u.disegnataAperta: k - 1 else: k) * u.corsa
  of "sx": u.pivot.rotation.y = -k * 1.75
  of "dx": u.pivot.rotation.y = k * 1.75
  of "su": u.pivot.rotation.x = -k * 1.6
  of "alza":   # i coperchi si sollevano nel vuoto e si allontanano un poco l'uno dall'altro
    u.pivot.position.y = k * u.corsa; u.pivot.position.z = k * 5 * u.verso
  of "solleva": u.pivot.position.y = k * u.corsa
  of "esce":   # il materasso sale sopra la fascia e poi esce dalla nicchia verso chi guarda
    const f = 0.3
    u.pivot.position.y = min(k / f, 1) * u.alzata
    u.pivot.position.z = max(0, (k - f) / (1 - f)) * u.corsa
  of "ribalta dx":   # l'antina scende, gira intorno alla cerniera e si affianca a destra
    u.pivot.rotation.z = k * PI
  else: discard

Il resto del visore segue la stessa forma del JavaScript di prima, con gli stessi nomi dove possibile (build è diventato costruisci, pose è posa): le viste (vista(nome)), il tocco che apre (unitAt), l'esploso. (Le sezioni in SVG con i piani di taglio c'erano e sono state tolte: le sezioni stanno nelle tavole.)

5. Come è stata fatta la migrazione

  1. Binding minimi. Elenco di tutto quello che il JavaScript usava di three.js e del browser, e un binding per ognuno, con i nomi originali.
  2. Traduzione riga per riga del JavaScript in src/cadero/visore.nim, tenendo la stessa struttura e gli stessi commenti, così il confronto è facile.
  3. I modelli nel browser. Il codice di models/parete.nim compila anche con nim js senza modifiche: il blocco nbJsFromCode chiama parete(...) e passa i modelli al visore. La conversione JSON resta solo per la vista 3D piccola dentro i documenti (viewer3d in web.nim).
  4. La pagina come documento nimib, con un renderer suo (copia di quello di nimib) che riempie il template del visore invece del tema dei documenti.
  5. Verifica. Vecchio e nuovo visore aperti in Chromium headless con le stesse azioni (vista iniziale, apri tutto, chiudi tutto, due tocchi su ante e cassetti, tavolino, antina aperta, prese sul comodino, dentro con la risega scoperta, senza carta da parati, senza fronte, esploso, sezioni, sezione selezionata e cursore spostato), con le animazioni disattivate: le 15 immagini sono identiche al pixel e gli SVG delle tre sezioni identici carattere per carattere.

Cose da sapere

6. Cosa diventa possibile

Visto che il modello si calcola nel browser, il prossimo passo naturale sono i cursori delle misure nel visore: cambiare un parametro (per esempio l'altezza della nicchia) e ricostruire la scena con parete(p) senza rigenerare la pagina.

Comandi

nimble docs                    # tutti i documenti, visore compreso (anche nim js)
nim r docsrc/visore3d.nim      # solo il visore -> docs/visore3d.html
nim r tools/visore3d.nim       # dopo il visore: tools/parete-3d.html per l'artefatto
## Come funziona il visore 3D e come è passato da JavaScript a Nim
## (-> docs/visore_nim.html).
import std/[os, strutils]
import nimib, tema
import nimib/highlight

nbInit(theme = useTema)
nb.title = "Come è fatto il visore 3D"

let radice = currentSourcePath().parentDir.parentDir

proc pezzo(file, inizio: string; fine = ""): string =
  ## Le righe di un file del repository dalla prima che contiene `inizio`
  ## fino alla prima riga vuota (o fino a quella che contiene `fine`, compresa).
  ## Così i pezzi di codice mostrati qui sono sempre quelli veri.
  var dentro = false
  for riga in lines(radice / file):
    if not dentro and inizio in riga: dentro = true
    if dentro:
      if fine == "" and riga.strip == "": break
      result.add riga & "\n"
      if fine != "" and fine in riga: break
  result = result.strip(leading = false)

proc mostra(file, inizio: string; fine = "") =
  ## Mostra un pezzo di codice colorato, con il nome del file sopra.
  let codice = pezzo(file, inizio, fine)
  nbRawHtml: "<p class=\"nb-small\"><code>" & file & "</code></p>\n" &
    "<pre><code class=\"nim hljs\">" & highlightNim(codice).strip(chars = {'\n'}) & "</code></pre>"

nbText: """
# Come è fatto il visore 3D

Il [visore 3D](visore3d.html) è la pagina dove la parete si gira, si apre e si
taglia (c'è anche quello della nostra idea, [Visore 3D - Idea](visore3d_idea.html),
stessa pagina con altri modelli). Fino a questa versione era una pagina HTML con circa 600 righe di
JavaScript scritte a mano; ora la logica è scritta in **Nim**, compilata in
JavaScript, e la pagina è un documento nimib con un solo blocco `nbJsFromCode`
(il blocco di nimib che compila Nim per il browser).

Questa pagina spiega come funziona il visore e come è avvenuto il passaggio.

## Perché

Prima il percorso dei dati era questo:

```
Nim (C)                      testo                     JavaScript
parete(p) ─► Model ─► toJson ─► 12 modelli JSON ─► visore3d.tpl.html (600 righe)
                              incollati nel template     campi rinominati: w, d, h,
                                                         holesTop.y = holesPiano.z ...
```

Il visore non vedeva il tipo `Part` di Caderò ma una sua copia in JSON, con
nomi diversi: `w`, `d`, `h` invece di `size.x`, `size.y`, `size.z`; nei fori
del piano `y` e `d` invece di `z` e `h`. Il pannello frontale riceveva come fori
anche le sagome dei frontali, ma solo nel JSON. Ogni nuova proprietà delle parti
(`apre`, `alzata`, `dentro`...) andava aggiunta in tre posti: il tipo in Nim, la
conversione in JSON, il JavaScript. E il JavaScript non aveva nessun controllo:
un nome sbagliato diventava `undefined` senza errori.

Ora:

```
Nim (JS, nel browser)
parete(p) ─► Model ─► avviaVisore ─► three.js
```

- **lo stesso codice Nim** che fa le tavole calcola i modelli anche nel browser:
  niente JSON, niente copie; ogni variante è una chiamata a `parete(...)`;
- il visore usa i tipi veri (`Part`, `Kind`, `RRect`) e il compilatore controlla
  nomi e tipi;
- i valori che prima erano scritti a mano nel JavaScript (viste, materiali, assi
  dei tagli) sono costanti e funzioni Nim con un nome e un commento;
- la pagina diventa un documento nimib come gli altri, generato da `nimble docs`.

## I pezzi

| file | cosa contiene |
| --- | --- |
| `src/cadero/js/three.nim` | i binding: le classi e i metodi di three.js che servono, visti da Nim |
| `src/cadero/js/pagina.nim` | le poche cose del browser che mancano in `std/dom` |
| `src/cadero/visore.nim` | il visore: scena, tocchi, esploso, in Nim |
| `docsrc/visore3d.nim`, `docsrc/visore3d_idea.nim` | le due pagine (falegnami e idea): modelli e testi, il blocco `nbJsFromCode` |
| `docsrc/pagina_visore.nim` | il renderer comune: template, carta da parati |
| `tools/visore3d.tpl.html` | stile, pulsanti, posti per la tavola e per lo script |
| `tools/visore3d.nim` | dalla pagina di Pages all'artefatto claude.ai |

## 1. Il blocco nbJsFromCode

Il cuore della pagina è questo blocco. Non gira quando si genera il documento:
nimib lo scrive in un file, lo compila con `nim js` e mette il JavaScript che
esce nella pagina, dentro un `<script>`. Le variabili tra parentesi (qui
`cartaSrc`, l'immagine della carta da parati) sono **catturate**: nimib le
trasforma in JSON durante la generazione e il blocco le ritrova nel browser.
"""

mostra("docsrc/visore3d_idea.nim", "nbJsFromCode(cartaSrc):", "quandoThreePronto")

nbText: """
Le varianti sono le stesse di prima (base o con tavolino, risega rivestita o
scoperta, antina, prese sul comodino: 12 combinazioni), ma ora si calcolano nel
browser invece di arrivare come 12 modelli JSON incollati nella pagina. Il file da
compilare nimib lo scrive nella cartella dei documenti (`docs/`): per questo gli
import cominciano con `../`.

## 2. three.js visto da Nim

three.js è una libreria JavaScript. Per usarla da Nim si scrivono dei
**binding**: dichiarazioni Nim che dicono al compilatore quale JavaScript
scrivere. Non serve un binding completo, solo le classi e i metodi che il visore
usa (circa 80). Due esempi dal file dei binding:
"""

mostra("src/cadero/js/three.nim", "  Vector3* = ref object of JsRoot")
mostra("src/cadero/js/three.nim", "proc newBox3*", "getSize")

nbText: """
- un tipo che eredita da `JsRoot` con il pragma `importc` è un oggetto JavaScript
  qualunque: i suoi campi (`x`, `position`, `visible`) si leggono e scrivono così
  come sono;
- un proc con `importjs` ha un **modello** di JavaScript: `#` è il prossimo
  argomento, `@` tutti i restanti. `newBox3()` diventa `new THREE.Box3()`,
  `b.getCenter(v)` resta `b.getCenter(v)`;
- i parametri "oggetto" di three.js (es. `{color: 0xe3c9a0, roughness: 0.8}`) si
  scrivono con `js{...}` di `std/jsffi`.

I binding hanno i nomi di three.js, così il codice del visore si legge con la
documentazione di three.js accanto.

### Come arriva three.js

three.js è copiato nel repository (`vendor/three/`, la versione 0.160.0) e la
generazione dei documenti lo mette in `docs/three/`, accanto alle pagine: nessuna
chiamata a un CDN. Si carica come modulo ES, con un importmap. Un modulo
parte *dopo* gli script normali, e quello generato da Nim è uno script normale.
Allora un piccolo modulo nella pagina mette three.js in `window.THREE` e manda un
evento; il codice Nim aspetta quell'evento con `quandoThreePronto`:
"""

mostra("tools/visore3d.tpl.html", "<script type=\"module\">", "</script>")
mostra("src/cadero/js/three.nim", "proc quandoThreePronto*")

nbText: """
## 3. Dalla parte alla scena

`costruisci(m)` scorre le parti del modello. Per ognuna sceglie la forma, la
mette nella scena con il suo materiale e i suoi spigoli e, se è apribile, la
aggancia al suo gruppo. Gli assi cambiano nome: in three.js l'alto è y e la
profondità va verso −z, quindi (x, y, z) del CAD diventano (x, z, −y).
"""

mostra("src/cadero/visore.nim", "proc geometria(p: Part)")

nbText: """
Per leggere il codice come prima, la parte ha anche i nomi brevi di una volta,
calcolati dai campi veri:
"""

mostra("src/cadero/visore.nim", "proc x(p: Part)")

nbText: """
Il pannello frontale nel 3D ha come fori anche le sagome di ante e cassetti, che
vanno a occuparli. Prima questo succedeva solo nella conversione in JSON; ora è
una funzione del modello, usata sia dal visore sia da `toJson`:
"""

mostra("src/cadero/model.nim", "proc foriVisti*")

nbText: """
## 4. Aprire e chiudere

Ogni gruppo apribile (`Unita`) ha due contenitori: `pivot`, che sta sullo spigolo
della cerniera e ruota o trasla, e `inner`, che riporta le parti al loro posto.
`k` va da 0 (chiuso) a 1 (aperto) e a ogni fotogramma insegue `target`; `posa`
dice dove sta il gruppo per un certo `k`, secondo il suo modo di aprirsi (`apre`):
"""

mostra("src/cadero/visore.nim", "proc posa(u: Unita)")

nbText: """
Il resto del visore segue la stessa forma del JavaScript di prima, con gli stessi
nomi dove possibile (`build` è diventato `costruisci`, `pose` è `posa`): le viste
(`vista(nome)`), il tocco che apre (`unitAt`), l'esploso. (Le sezioni in SVG con i piani di
taglio c'erano e sono state tolte: le sezioni stanno nelle tavole.)

## 5. Come è stata fatta la migrazione

1. **Binding minimi.** Elenco di tutto quello che il JavaScript usava di three.js e
   del browser, e un binding per ognuno, con i nomi originali.
2. **Traduzione riga per riga** del JavaScript in `src/cadero/visore.nim`,
   tenendo la stessa struttura e gli stessi commenti, così il confronto è facile.
3. **I modelli nel browser.** Il codice di `models/parete.nim` compila anche con
   `nim js` senza modifiche: il blocco `nbJsFromCode` chiama `parete(...)` e
   passa i modelli al visore. La conversione JSON resta solo per la vista 3D
   piccola dentro i documenti (`viewer3d` in `web.nim`).
4. **La pagina come documento nimib**, con un renderer suo (copia di quello di
   nimib) che riempie il template del visore invece del tema dei documenti.
5. **Verifica.** Vecchio e nuovo visore aperti in Chromium headless con le stesse
   azioni (vista iniziale, apri tutto, chiudi tutto, due tocchi su ante e cassetti,
   tavolino, antina aperta, prese sul comodino, dentro con la risega scoperta,
   senza carta da parati, senza fronte, esploso, sezioni, sezione selezionata e
   cursore spostato), con le animazioni disattivate: le 15 immagini sono identiche
   al pixel e gli SVG delle tre sezioni identici carattere per carattere.

### Cose da sapere

- **Closure nei cicli.** In JavaScript `for (const name of ...)` dà a ogni giro la
  sua variabile; in Nim no: una closure creata in un ciclo vede l'ultima. Per
  questo i pulsanti creati in un ciclo usano `closureScope:` con una copia della
  variabile (il primo tentativo aveva proprio questo errore: "Base" riapriva il
  tavolino).
- **Stringhe.** In Nim JS una `string` non è una stringa JavaScript: per DOM e
  three.js si passa `cstring`.
- **Conversioni di tipo.** Un evento DOM non ha le informazioni di tipo di Nim:
  per passare da `Event` a un evento del puntatore si usa `cast`, non la
  conversione normale.
- **`importjs` vuole un modello.** Per leggere una costante come
  `THREE.RepeatWrapping` il modello va tra parentesi: `"(THREE.RepeatWrapping)"`.
- **Peso della pagina.** Il JavaScript generato (visore + modello + libreria
  standard di Nim) prende il posto dei 12 modelli JSON: la pagina passa da circa
  350 KB a circa 495 KB (la carta da parati ne pesa 60). Con più varianti la
  differenza cresce a favore di Nim: una variante in più è una chiamata, non un JSON.
- **Il blocco è ricopiato come testo.** nimib trasforma il blocco `nbJsFromCode`
  in testo e lo ricompila: un'espressione come `(if a: "x" else: "") & (if b: ...)`
  su più righe non sopravvive alla copia. Meglio istruzioni semplici, una per riga.
- **Compilazione.** `nimble docs` ora chiama anche `nim js` per il blocco del
  visore: qualche secondo in più.

## 6. Cosa diventa possibile

Visto che il modello si calcola nel browser, il prossimo passo naturale sono i
**cursori delle misure** nel visore: cambiare un parametro (per esempio l'altezza
della nicchia) e ricostruire la scena con `parete(p)` senza rigenerare la pagina.

## Comandi

```sh
nimble docs                    # tutti i documenti, visore compreso (anche nim js)
nim r docsrc/visore3d.nim      # solo il visore -> docs/visore3d.html
nim r tools/visore3d.nim       # dopo il visore: tools/parete-3d.html per l'artefatto
```
"""

nbSave