Finiamo il Loopcamp?

Qualche tempo fa, avevo messo a disposizione pubblicamente uno dei miei cavalli di battaglia per quanto riguarda l’introduzione a Dynamo, ovvero la creazione di questa installazione. Loopcamp is a stop, a shelter against Nevada’s frequent storms at the Burning Man festival. With the festival arises a spontaneous city of 52,000 people settled on an ancient […]

Qualche tempo fa, avevo messo a disposizione pubblicamente uno dei miei cavalli di battaglia per quanto riguarda l’introduzione a Dynamo, ovvero la creazione di questa installazione.

Loopcamp is a stop, a shelter against Nevada’s frequent storms at the Burning Man festival.
With the festival arises a spontaneous city of 52,000 people settled on an ancient Indian reserve by the now dried Lohontan Lake, 700 feet above sea level.
Black Rock City is the last social utopia since the creation of Israel’s first Kibboutz.

Mi sono fermata prima dell’impostazione dei raggi, limitandomi a mostrare il workflow completo fino all’impostazione delle altezze. Oggi voglio finire l’installazione con voi: è una buona occasione per vedere ancora un po’ di manipolazione delle liste e qualche trucco per la gestione delle distanze.

1. La storia fino ad ora

Fino al punto in cui vi avevo lasciato, il workflow può essere riassunto in questo modo:

  • abbiamo fatto il tracciamento generale dell’installazione e definito il numero di cilindri che vogliamo;
  • sul cerchio perimetrale, abbiamo posizionato un numero corrispondente di punti ad uguale distanza tra loro (possiamo usare la distanza del segmento o della corda, oppure posizionarli secondo una suddivisione parametrica da 0 a 1);
  • calcolato il raggio e ipotizzando di avere tutti i raggi uguali, abbiamo posizionato delle famiglie usando i punti come riferimento e abbiamo poi impostato il parametro di tipo “raggio” secondo il calcolo effettuato [questo è il punto cui dovremo aggiungere un livello di complessità oggi];
  • abbiamo poi creato delle altezze casuali in un range predefinito che tiene conto di altezza minima e altezza massima, le abbiamo rimescolate e assegnate al parametro di istanza per l’altezza.

2. Lavoriamo con i raggi

Come sempre, dimentichiamoci per un istante le famiglie e lavoriamo con la geometria: una volta capito cosa vogliamo fare, una volta descritta adeguatamente la geometria che vogliamo ottenere, potremo tornare a lavorare con i nostri componenti Revit. Uno dei metodi per calcolare il raggio era, semplicemente, misurare la distanza tra i primi due punti. Ricordiamoci che per avere cerchi che si toccano la nostra necessità è di misurare la corda e dividerla per due. Il procedimento è ancora valido e ci serve per calcolare quello che chiameremo raggio medio, intorno al quale individueremo delle variazioni per “muovere” ulteriormente la nostra installazione.

Iniziamo a ragionare su un numero più piccolo di elementi, e vediamo se riusciamo a impostare un ragionamento che ci consenta di avere un numero variabile di raggi, ma non variabile quanto le altezze.

2.1. Metodi per ottenere elementi “casuali”

Parlo di elementi “casuali”, con le opportune virgolette, perché sappiamo che il concetto di “casuale” è un concetto non così banale e ciò che un computer ci può restituire, nella maggior parte dei casi, è un numero non facilmente prevedibile. Per ulteriori approfondimenti, consiglio ad esempio di leggere qui.

Can a computer generate a truly random number?
It depends what you mean by random…

Esistono almeno quattro metodi, dicevo, per ottenere numeri “casuali”: Random (seed), Random (value1, value2), RandomList, Rand.
Vedamoli uno per uno e vediamo in quali circostanze ci potrebbero essere utili. Per approfondire i concetti vi consiglio anche questo post di bimwood (vecchiotto ma ancora valido) e per scendere ancora più in profondità nella tana del bianconiglio vi consiglio questo altro post di Stellworks (ancora più vecchio, ma più astratto).

2.1.1. Random (seed) e Rand

Random (seed) appare come Math.Random, esattamente come Random (value1, value2), ma in realtà è molto più simile a Rand (che appare come Math.Rand): entrambi generano un valore casuale da 0 a 1, ma del primo è possibile controllare il “Seed”, ovvero la variabile sulla quale lui lavora per generare l’apparente casualità. Il seed in un workflow che richiede valori realmente casuali, come spiegato nell’articolo della MIT School of Engineering, è bene provenga da una fonte esterna a sua volta altamente variabile.

“One thing that traditional computer systems aren’t good at is coin flipping,” says Steve Ward, Professor of Computer Science and Engineering at MIT’s Computer Science and Artificial Intelligence Laboratory. “They’re deterministic, which means that if you ask the same question you’ll get the same answer every time. In fact, such machines are specifically and carefully programmed to eliminate randomness in results. They do this by following rules and relying on algorithms when they compute.”

Potremmo usare uno di questi due nodi, ma dovremmo ripeterli in un loop e creare una lista con i valori che ci occorrono per variare il raggio. Inoltre, dovremmo dividere parametricamente ogni singolo arco di circonferenza, posizionando un ulteriore punto con il metodo Place Point at Parameter e poi dovremmo misurare la distanza tra il centro e questo punto per individuare il raggio casuale.

Ma anche no.

2.1.2. Random (value1, value2)

Il metodo richiede due input, un valore minimo e un valore massimo, e produce un valore “casuale” all’interno di quell’intervallo.

Nel nostro caso, abbiamo un valore medio. Potremmo utilizzarlo per individuare quindi qual è il massimo raggio che possiamo permetterci e il minimo valore che vogliamo consentire, in modo da avere il medesimo numero di cilindri anche alla fine della variazione dei raggi.

Potrebbe andare bene. Teniamolo da parte.

2.1.3. Random (list)

L’ultimo metodo genera una lista di valori casuali da 0 a 1 e quindi ci riporta alla suddivisione parametrica. Ha il vantaggio di produrci una lista, semplificando quindi quel passaggio, ma rimane lo svantaggio cui accennavo nella descrizione al punto 1: utilizzando questo metodo, bisognerà calcolare la dimensione effettiva del raggio per darla in pasto al parametro nella famiglia, lavorando in ambiente di progetto. Diverso sarebbe se lavorassimo nell’ambiente delle masse e fossimo riusciti a parametrizzare il raggio dei cilindri secondo la suddivisione parametrica della curva, che ragiona proprio in range 0..1.

Inutile piangere sulle adattive versate.

2.2. Scegliamone uno

Personalmente ho deciso di scegliere Random (list) e in particolare ho deciso che il mio valore di raggio “medio” sarà il valore massimo, mentre il valore minimo sarà la metà di quel valore. A questo punto potete scegliere i tetti che preferite, anche variandoli a valle del risultato finale.

Dato che però non stiamo generando una lista, c’è ancora un ragionamento che dobbiamo fare riguardo a quanti diversi raggi vogliamo all’interno di questo range.
Ipotizziamo di volerne 6 variazioni (numero totalmente arbitrario che potremo cambiare in seguito).
Il modo più pratico è utilizzare il nodo “Of Repeated Item” e replicare uno dei due valori di input di “Math.Random”, non ha nessuna importanza quale. A questo punto, avendo come input una lista, impostando il lacing del nodo a “longest” ci troveremo con un’iterazione dell’operazione e, quindi, con 6 valori casuali.

A noi però non servono 6 valori: serve una ripetizione di quei 6 valori per un numero di volte che ci consenta di coprire la totalità dei cilindri. Vedremo che non è esattamente così, ma per il momento cerchiamo di ottenere una lista con quei valori, ripetuti e rimescolati, con lunghezza pari alla quantità di cilindri impostata ad inizio script. Esiste un nodo simile a List.OfRepeatedItem, che anziché ripetere un’operazione si limita a replicare la lista. Si tratta di List.Cycle. La quantità di volte richiesta è una semplice divisione: il numero dei cilindri diviso la quantità di raggi che abbiamo deciso di generare. Sarà un numero con la virgola (Double), ma non c’è bisogno di arrotondare (cosa che potrebbe essere fatta con Math.Floor o Math.Ceiling): anche nel caso di numeri con la virgola, l’input ad Amount approssimerà da solo all’intero più vicino.
Otterremo una ripetizione regolare, gli stessi 6 valori in fila: suggerisco di dare una ulteriore rimescolata.

E, dato che non mi piace lavorare con liste più lunghe del necessario, vorrei anche troncare la lista in modo da avere esattamente un numero di valori pari al numero dei cilindri. Può essere fatto tranquillamente con List.Chop.

Il risultato è qualcosa del genere. Attenzione perché List.Chop crea delle sottoliste anche quando la lunghezza è già corretta, ovvero quando la divisione tra numero di cilindri e numero di variazioni restituiva un numero intero. Conviene quindi posizionare un codeblock che prenda la lista all’item 0.

x[0]

Nel provare ad applicare questa lista di valori ai raggi, però, ci renderemo immediatamente conto di una cosa: il nostro sistema originale per avere cilindri che si toccano, creando un sistema senza buchi, è venuto meno.

Buchi! Buchi nel mio loopcamp!

2.3. Tappiamo i buchi

La situazione è evidentemente incresciosa: non possiamo andarcene in giro con un Loopcamp pieno di buchi.
Se fino a questo punto i metodi possibili riguardavano fondamentalmente il calcolo reale o parametrico delle variazioni per i raggi, automaticamente discendente dalla nostra scelta di lavorare con famiglie tradizionali in ambiente di progetto anziché lavorare in ambiente di massa con componenti adattivi o direttamente nell’ambiente di una grande famiglia adattiva.
Ora la faccenda si complica: tappare i buchi richiede una certa dose di pensiero laterale e ci sono miriadi di possibilità. Vi propongo la soluzione che normalmente presento a lezione, perché richiede un bel po’ di lavoro con le liste e di fatto lavorare con le liste è una delle competenze più importanti quando si vuole usare Dynamo.

Potrei decidere di tappare i buchi aggiungendo altri cilindri nel mezzo, ma si tratta di una soluzione che compromette parte dell’architettura dello script, dato che sto utilizzando quel numero iniziale per fare diverse cose. Preferisco di no.
A questo punto però portremmo anche decidere che i cilindri pari, all’interno di quella lista, sono sbagliati. Cancellando i cilindri pari e riuscendo a calcolare quale dovrebbe essere il raggio per tappare il buco, dovremmo riuscire a risolvere la situazione.
Una cosa per volta.

2.3.1. Filtriamo gli elementi dispari

Si tratta di un’operazione che dovremo fare sui valori del raggio, ma per il momento facciamolo sui cilindri finiti, per avere un feedback visivo di quello che stiamo facendo.

Anche in questo caso, ci sono diversi modi di farlo: potrei creare una lista alternata di Yes/No e utilizzarla per una Boolean Mask, ma voglio utilizzare gli indici. Il modo più rapido è quindi generare una sequenza da 1 a n, dove n è sempre il numero totale di cilindri, con passo 2.

Eccoli, tutti i miei indici dispari

Avendo questi indici, posso usarli con il nodo Remove.ItemAtIndex, per vedere di nascosto l’effetto che fa.

Eccoli qui. So già che avrò un problema in chiusura, con il primo e l’ultimo elemento, ma vedremo più tardi come gestirli.

2.3.2. Calcoliamo il raggio degli elementi pari

A questo punto il mio problema dovrebbe essere evidente: devo aumentare il raggio degli elementi pari in modo che arrivi a colmare la distanza tra i cilindri dispari e spostarne il posizionamento in modo che siano sempre a metà dell’arco lasciato vuoto.

La vedete la soluzione? E’ lì.

Posso quindi dimenticarmi dei cerchi pari, non mi servono: ne devo tracciare di nuovi. Procediamo quindi a usare lo stesso nodo Remove.ItemAtIndex in modo da ritrovarmi solo con i cerchi che effettivamente vengono estrusi. Sto iniziando quindi a spostarlo a monte: anziché filtrare il prodotto finito, sto filtrando le geometrie di costruzione. Non mi piace lavorare per niente.

Stiamo ancora facendo lavorare lo script inutilmente: stiamo ad esempio generando una quantità di raggi pari al doppio di quello che ci serve, ma possiamo ottimizzarlo in seguito.
Usando una serie di nodi che conosciamo già (List.Chop e List.GetItemAtIndex), possiamo isolare i punti a coppie.

L’arco ci serve per trovare il centro del cerchio mancante. Il cerchio dovrà però essere posizionato a metà della parte libera di arco, quindi occorre un passaggio in più: su quella curva dobbiamo posizionare punti all’intersezione dei cerchi e il punto che ci interessa sarà sulla metà dell’arco rimanente. Troviamo i punti di intersezione tra i cerchi e gli archi.

Li vedete quei punti azzurri?

 

Usiamo i punti per il nuovo arco (spegnendo il precedente) ed ecco che abbiamo ciò che ci serve. La lista è strutturata buffa, a causa di come lavora il nodo Geometry.Intersect, quindi dobbiamo lavorare al livello 3. Conviene appiattire tutto, terminata l’operazione.

Li vedete? Quelli sono i cerchi che stiamo cercando.

Quei punti non ci occorrono più, perché ne posizioneremo degli altri, proprio a metà di quell’arco. Abbiamo un nodo perfetto per posizionare punti a metà delle cose: è Curve.PointAtParameter con 0.5 come parametro.
Per vedere un workflow diverso da quelli già presentati, possiamo estrarre il raggio usando il nodo Curve.SegmentLengthBetweenParameters: usiamo 0 come start parameter e 0.5 come end parameter, oppure 0.5 come start e 1 come end. Questo metodo è il più accurato per estrarre questo tipo di valore.

Ecco che quindi abbiamo ricavato alcuni dei nostri cerchi mancanti.

Ma perché solo alcuni e non tutti?

2.3.3. Estendere il metodo

Abbiamo ancora dei buchi nel nostro Loopcamp. Meno di quelli che avevamo originariamente, chiaro, ma comunque dei buchi.
Il motivo ha origine quando abbiamo usato List.Chop con 2 come valore e abbiamo creato le coppie di punti: in questo modo abbiamo isolato i punti 1 e 2, 3 e 4, 5 e 6, ma non abbiamo mai considerato le altre coppie. Abbiamo quindi dei buchi in 2 e 3, 4 e 5 e così via.

Un modo semplice per ottenere quello che vogliamo è ripetere la stessa operazione sulle stesse liste ma dopo aver effettuato uno shift di 1 indice.

Faccio la stessa cosa anche sui cerchi e sostituisco l’input di punti e cerchi per la creazione degli archi, il posizionamento dei punti centrali, il calcolo dei raggi e il tracciamento dei cerchi.

Si potrebbero fare diverse ottimizzazioni, ma diciamo che a questo punto la situazione è completa.

Consiglio di fare un gruppo riassuntivo (List.Join) di tre elementi: tutti i cerchi, tutti i raggi e tutti i centri nello stesso ordine.

Ecco il punto cui siamo.

3. Torniamo alle nostre famiglie

Molte parti di questo script che generano geometrie possono essere cancellate. Ma non voglio farlo. Ci aiutano per capire cosa stiamo facendo e ci aiuteranno sempre. Può però essere un buon momento per tornare a lavorare sulle famiglie. Il primo passò è naturalmente trasformare il raggio in un parametro di istanza. Potremmo anche creare dei tipi per ogni raggio (in fondo ne abbiamo solo 6) ma non è questo il giorno.

Detto ciò:

  1. Usiamo i nuovi punti per posizionare le famiglie;
  2. Usiamo le altezze per impostare il parametro corrispondente;
  3. Usiamo i nuovi raggi per impostare il parametro corrispondente.
Passo 1: in nero vedete le famiglie, ancora con raggio e altezza generiche, mentre in blu vedete il “fantasma” di ciò che Dynamo sta costruendo
Passo 2: altezze impostate
Passo 3: raggi impostati.

4. Ottimizziamo il tutto

Rimangono alcuni punti di ottimizzazione. Vengono replicati ad esempio alcuni nodi nella parte di suddivisione a coppie delle liste e in questo momento non stiamo posizionando i cilindri in ordine, che sia orario o antiorario, quindi otterremmo codici buffi laddove provassimo ad assegnare un parametro Mark. Ma per il momento direi che può bastare.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.