10 janv. 2026

[Dynamo += UltraLeap] Bonne Année !

    




Profitons de ce début d'année pour re-explorer les frontières entre le monde physique et le numérique. Aujourd'hui, nous ressortons le contrôleur UltraLeap avec Dynamo

#DynamoBIM #Python #LeapMotion #UltraLeap #AutodeskExpertElite #AutodeskCommunity 

if this article is not in your language, use the Google Translate widget ⬈ (bottom of page for Mobile version )


Contexte

A l'occasion d'un Challenge Dynamo sur le forum, voici une carte de vœux 2026 un peu spéciale.

Pour faire suite à un article précédent, voici une version modifiée dans laquelle on crée un solide de type « Texte ».


  • Génération du Texte 3D

Pour transformer un texte en solide exploitable, nous contournons les limitations standard en utilisant la classe GraphicsPath du namespace System.Drawing.Drawing2D.

L'idée est de récupérer les contours vectoriels de la police (ici Arial), de les convertir en points DS.Point, puis de reconstruire des PolyCurves pour enfin générer un solide ( Thicken).

code Python

import clr
import sys
import System
clr.AddReference("System.Numerics")
#
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
import Autodesk.DesignScript.Geometry as DS  
    
clr.AddReference('System.Drawing')
import System.Drawing
from System.Drawing import *
from System.Drawing.Drawing2D import *

    
text_value = "2026"
height_mm = 10
view_scale = 50
draw_contours = True
#
height = (height_mm * view_scale) * 0.00328083989501312 # convert for text 3d
#
filregionTypeName = "Uni2 - Noir"


class CManager:
    """
    a custom context manager for Disposable Object
    """
    def __init__(self, obj):
        self.obj = obj
        
    def __enter__(self):
        return self.obj
        
    def __exit__(self, exc_type, exc_value, exc_tb):
        self.obj.Dispose()
        if exc_type:
            error = f"{exc_value} at line {exc_tb.tb_lineno}"
            raise ValueError( error)
        return self

points = []
types = []
with CManager(System.Drawing.Font("Arial", height, FontStyle.Regular)) as font:
    with CManager(GraphicsPath()) as gp:
        with CManager(StringFormat()) as sf:
            sf.Alignment = StringAlignment.Center
            sf.LineAlignment = StringAlignment.Center
            gp.AddString(text_value, font.FontFamily, System.Convert.ToInt32(font.Style), font.Size, PointF(0, 0), sf)
            # convert to loop array
            points = list(gp.PathPoints)
            types = list(gp.PathTypes)
# get all indice at 0 value
indices = [i for i, value in enumerate(types) if value == 0]
# split points at indices
array_pt = [points[i:j] for i, j in zip([0]+indices, indices+[None])]
# convert to XYZ
array_pt_rvt = [[DS.Point.ByCoordinates(p.X, -p.Y, 0) for p in subpoints] for subpoints in array_pt]
# purge point and create PolyCurve then Solid by letter
lst_curvloop = []
lst_solid = []
for idx, lstpt in enumerate(array_pt_rvt):
    if len(lstpt) > 0:
        ptToPull = [lstpt.pop(0)]
        curves = []
        for pt in lstpt:
            if all(pt.DistanceTo(p) > 0.01 for p in ptToPull):
                ptToPull.append(pt)
        loop = DS.PolyCurve.ByPoints(ptToPull, True)
        lst_curvloop.append(loop)
        solid = loop.Patch().Thicken(0.7)
        if idx in [3, 6]:
            lst_solid[-1] = lst_solid[-1].Difference(solid)
        else:
            lst_solid.append(solid)
            
geo = DS.Solid.ByUnion(lst_solid)
# align solid to same coordinate system of UltraLeap
geo = geo.Rotate(DS.Point.ByCoordinates(0, 0, 0), DS.Vector.XAxis(), 90)
geo = geo.Rotate(DS.Point.ByCoordinates(0, 0, 0), DS.Vector.YAxis(), 180)
geo = geo.Rotate(DS.Point.ByCoordinates(0, 0, 0), DS.Vector.ZAxis(), 180)
    
OUT = geo


  • L'Interaction : La "Magie" UltraLeap

le code Python est quasiment identique à celui de mon précédent article  :

  • Logique de détection
Le script calcule la distance entre le pouce et l'index. Si cette distance est inférieure à un seuil, l'action est déclenchée.
    • Pincement simple : Détection de la position dans l'espace.
    • Double pincement (deux mains) : En utilisant la distance entre les deux mains, on définit une BoundingBox dynamique qui ajuste l'échelle (Scale) du texte "2026".


# Load the Python Standard and DesignScript Libraries
import sys
import clr
import System
clr.AddReference('ProtoGeometry')
import Autodesk.DesignScript.Geometry as DS

import sysconfig
# standard library path
sys.path.append(sysconfig.get_path("platstdlib"))
# site-package library path
sys.path.append(sysconfig.get_path("platlib"))
import leap
from leap import datatypes as ldt
import time

from io import StringIO    
sys.stdout = StringIO()

class MyListener(leap.Listener):
    def __init__(self):
        self.hands_format = "Skeleton"
        self.lst_points = []
        self.lst_cuboid = []
        self.radius = 2
        
    def on_connection_event(self, event):
        pass
        #print("Connected")

    def on_device_event(self, event):
        try:
            with event.device.open():
                info = event.device.get_info()
        except leap.LeapCannotOpenDeviceError:
            info = event.device.get_info()

        #print(f"Found device {info.serial}")
        
    def location_end_of_finger(self, hand: ldt.Hand, digit_idx: int) -> ldt.Vector:
        digit = hand.digits[digit_idx]
        return digit.distal.next_joint
        
    def sub_vectors(self, v1: ldt.Vector, v2: ldt.Vector) -> list:
        return map(float.__sub__, v1, v2)
        
    def fingers_pinching(self, thumb: ldt.Vector, index: ldt.Vector):
        diff = list(map(abs, self.sub_vectors(thumb, index)))
    
        if diff[0] < 20 and diff[1] < 20 and diff[2] < 20:
            return True, diff
        else:
            return False, diff
        
    def get_joint_position(self, bone):
        if bone:
            #return int(bone.x + (self.screen_size[1] / 2)), int(bone.z + (self.screen_size[0] / 2))
            return bone.x , bone.y , bone.z 
        else:
            return None

    def on_tracking_event(self, event):
        global solid_2026
        global bbx_2026
        geometries = []
        pinch_pts = []
        pinch_hand_type = set()
        #print(f"Frame {event.tracking_frame_id} with {len(event.hands)} hands.")
        for i in range(0, len(event.hands)):
            hand = event.hands[i]
            hand_type = "left" if str(hand.type) == "HandType.Left" else "right"
            for index_digit in range(0, 5):
                digit = hand.digits[index_digit]
                for index_bone in range(0, 4):
                    bone = digit.bones[index_bone]
                    if self.hands_format == "Skeleton":
                        #print("Skeleton")
                        wrist = self.get_joint_position(hand.arm.next_joint)
                        elbow = self.get_joint_position(hand.arm.prev_joint)
                        if wrist:
                            #print("wrist")
                            pta = DS.Point.ByCoordinates(*wrist)
                            geometries.append(DS.Sphere.ByCenterPointRadius(pta, self.radius ))

                        if elbow:
                            #print("elbow")
                            ptb = DS.Point.ByCoordinates(*elbow)
                            geometries.append(DS.Sphere.ByCenterPointRadius(ptb, self.radius ))

                        if wrist and elbow:
                            #print("wrist and elbow")
                            try:
                                geometries.append(DS.Line.ByStartPointEndPoint(pta, ptb))
                            except Exception as ex:
                                 pass

                        bone_start = self.get_joint_position(bone.prev_joint)
                        bone_end = self.get_joint_position(bone.next_joint)
                        #print(f"{bone_start=}")
                        #print(f"{bone_end=}")
                        ptc = DS.Point.ByCoordinates(*bone_start)
                        geometries.append(DS.Sphere.ByCenterPointRadius(ptc, self.radius ))
                        ptd = DS.Point.ByCoordinates(*bone_end)
                        geometries.append(DS.Sphere.ByCenterPointRadius(ptd, self.radius ))
                        try:
                            geometries.append(DS.Line.ByStartPointEndPoint(ptc, ptd))
                        except Exception as ex:
                             pass
                        #                
                        # test if pinching
                        thumb = self.location_end_of_finger(hand, 0)
                        index = self.location_end_of_finger(hand, 1)
                        pinching, array = self.fingers_pinching(thumb, index)
                        #print(f"{pinching=}", array)
                        if pinching:
                            pinch_pts.append(DS.Point.ByCoordinates(*array))
                            pinch_hand_type.add(hand_type)
            #
            print(len(geometries))
            if len(pinch_hand_type) == 2 and len(geometries) >= 141:
                bbx = DS.BoundingBox.ByGeometry([geometries[21], geometries[141]])
                #
                try:
                    center = ( 
                                (bbx.MinPoint.X + bbx.MaxPoint.X) / 2, 
                                (bbx.MinPoint.Y + bbx.MaxPoint.Y) / 2, 
                                (bbx.MinPoint.Z + bbx.MaxPoint.Z) / 2
                            )
                    #
                    factorX = (bbx.MaxPoint.X - bbx.MinPoint.X) / (bbx_2026.MaxPoint.X - bbx_2026.MinPoint.X) 
                    factorY = (bbx.MaxPoint.Y - bbx.MinPoint.Y) / (bbx_2026.MaxPoint.Y - bbx_2026.MinPoint.Y)
                    factorZ = (bbx.MaxPoint.Z - bbx.MinPoint.Z) / (bbx_2026.MaxPoint.Z - bbx_2026.MinPoint.Z)
                    #
                    new_solid_2026 = solid_2026.Scale(factorX, factorY, factorZ)
                    new_solid_2026 = new_solid_2026.Translate(*center)
                    #
                    self.lst_cuboid.append(new_solid_2026)
                except Exception as ex:
                    print(ex)
            #
            if len(geometries) > 0:
                self.lst_points.append(geometries)
            

def main():
    global connection
    my_listener = MyListener()
    #connection = leap.Connection()
    connection.add_listener(my_listener)
    with connection.open():
        connection.set_tracking_mode(leap.TrackingMode.Desktop)
        time.sleep(0.1)
    connection.remove_listener(my_listener)
    if len(my_listener.lst_points) > 0 and len(my_listener.lst_cuboid) > 0:
        return my_listener.lst_points[-1], my_listener.lst_cuboid[-1]
    elif len(my_listener.lst_points) > 0 and len(my_listener.lst_cuboid) == 0:
        return my_listener.lst_points[-1], None
    else:
        return [], None

connection = IN[0]
solid_2026 = IN[1]
bbx_2026 = solid_2026.BoundingBox
diagonal_original_2026 = bbx_2026.MaxPoint.DistanceTo(bbx_2026.MinPoint)

geoms, new_cuboid = main()
datam = []


dataKey = "LstGeoms"
if len(geoms) == 0:
    System.AppDomain.CurrentDomain.SetData("_Dyn_Wireless_{}".format(dataKey), [])
print(new_cuboid)
if isinstance(new_cuboid, DS.Solid):
    # set Data to memory
    data = new_cuboid
    # get Data in memory
    datam = System.AppDomain.CurrentDomain.GetData("_Dyn_Wireless_{}".format(dataKey))
    if datam is None and isinstance(data, DS.Cuboid):
        System.AppDomain.CurrentDomain.SetData("_Dyn_Wireless_{}".format(dataKey), [data])
    elif isinstance(data, DS.Solid):
        try:
            datam.append(data)
            # clean solids
            temp = []
            for solid in datam[::-1]:
                if all(not solid.DoesIntersect(s) for s in temp):
                    temp.append(solid)
            
            System.AppDomain.CurrentDomain.SetData("_Dyn_Wireless_{}".format(dataKey), temp)
        except Exception as ex:
            print(ex)
    else: 
        pass

#
datam = System.AppDomain.CurrentDomain.GetData("_Dyn_Wireless_{}".format(dataKey))
sys.stdout.seek(0)
printResult =  sys.stdout.read()
OUT =  geoms, printResult, datam
# reset the sys.stdout
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__


Le résultat en vidéo !




Meilleurs vœux et une excellente année 2026 à tous !

0 commentaires:

Enregistrer un commentaire