From c4282bca1d6179c0e7180b60de39309d84bf704b Mon Sep 17 00:00:00 2001 From: hadware Date: Thu, 28 May 2026 02:11:22 +0200 Subject: [PATCH] =?UTF-8?q?Ajout=20du=20rendu=20isom=C3=A9trique=20SVG=20p?= =?UTF-8?q?our=20build123d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {openscad => archive/openscad}/couvercle.scad | 0 .../openscad}/inferieure.scad | 0 .../openscad}/parametres.scad | 0 .../openscad}/superieure.scad | 0 .../tractrudeuse.ipynb | 0 iso_render.py | 569 ++++++++++++++++++ ocp_screenshot.py | 280 +++++++++ poignee.ipynb | 312 ++++++++++ raiser.ipynb | 84 +++ test_render.ipynb | 332 ++++++++++ tractocadre.ipynb | 10 +- tractrudeuse_v2.ipynb | 165 +---- 12 files changed, 1612 insertions(+), 140 deletions(-) rename {openscad => archive/openscad}/couvercle.scad (100%) rename {openscad => archive/openscad}/inferieure.scad (100%) rename {openscad => archive/openscad}/parametres.scad (100%) rename {openscad => archive/openscad}/superieure.scad (100%) rename tractrudeuse.ipynb => archive/tractrudeuse.ipynb (100%) create mode 100644 iso_render.py create mode 100644 ocp_screenshot.py create mode 100644 poignee.ipynb create mode 100644 raiser.ipynb create mode 100644 test_render.ipynb diff --git a/openscad/couvercle.scad b/archive/openscad/couvercle.scad similarity index 100% rename from openscad/couvercle.scad rename to archive/openscad/couvercle.scad diff --git a/openscad/inferieure.scad b/archive/openscad/inferieure.scad similarity index 100% rename from openscad/inferieure.scad rename to archive/openscad/inferieure.scad diff --git a/openscad/parametres.scad b/archive/openscad/parametres.scad similarity index 100% rename from openscad/parametres.scad rename to archive/openscad/parametres.scad diff --git a/openscad/superieure.scad b/archive/openscad/superieure.scad similarity index 100% rename from openscad/superieure.scad rename to archive/openscad/superieure.scad diff --git a/tractrudeuse.ipynb b/archive/tractrudeuse.ipynb similarity index 100% rename from tractrudeuse.ipynb rename to archive/tractrudeuse.ipynb diff --git a/iso_render.py b/iso_render.py new file mode 100644 index 0000000..59ba1ec --- /dev/null +++ b/iso_render.py @@ -0,0 +1,569 @@ +""" +iso_render.py +============= +Rendu isométrique SVG pour build123d. + +Usage rapide +------------ + from build123d import * + from iso_render import IsoRenderer, PaperSheet, Scene + + model = Box(40, 30, 20) + + sheet = PaperSheet( + svg_content=open("content.svg").read(), # SVG couleur + width=80, height=110, # dimensions réelles (mm) + position=(0, 0, 0), # centre de la feuille + rotation_x=0, rotation_y=0, rotation_z=0 # rotations en degrés + ) + + scene = Scene() + scene.add_shape(model, base_color="#5B8DB8", position=(0, 0, 10)) + scene.add_paper(sheet) + + renderer = IsoRenderer(width=800, height=600, iso_angle=30) + renderer.render(scene, "output.svg") +""" + +import math +import re +import xml.etree.ElementTree as ET +from dataclasses import dataclass, field +from typing import Optional +import numpy as np + + +# ───────────────────────────────────────────── +# Projection isométrique +# ───────────────────────────────────────────── + +def iso_matrix(angle_deg: float = 30) -> np.ndarray: + """ + Matrice de projection isométrique 3D → 2D. + angle_deg : angle d'inclinaison vertical (30° = iso classique) + La rotation horizontale est fixée à 45°. + """ + a = math.radians(45) # rotation autour Y (azimut) + b = math.radians(angle_deg) # inclinaison + + # Rotation Y (azimut 45°) + Ry = np.array([ + [ math.cos(a), 0, math.sin(a)], + [ 0, 1, 0], + [-math.sin(a), 0, math.cos(a)], + ]) + # Rotation X (inclinaison) + Rx = np.array([ + [1, 0, 0], + [0, math.cos(b), -math.sin(b)], + [0, math.sin(b), math.cos(b)], + ]) + M = Rx @ Ry + # On ne garde que les 2 premières lignes (projection sur le plan écran) + return M[:2, :] # shape (2, 3) + + +def project(points: np.ndarray, M: np.ndarray) -> np.ndarray: + """points: (N,3) → projected: (N,2)""" + return (M @ points.T).T + + +# ───────────────────────────────────────────── +# Shading +# ───────────────────────────────────────────── + +# Direction de la lumière (normalisée) +_LIGHT = np.array([0.6, 0.8, 1.0]) +_LIGHT /= np.linalg.norm(_LIGHT) + + +def shade_color(base_hex: str, normal: np.ndarray, ambient: float = 0.35) -> str: + """Applique un shading lambertien à une couleur hex.""" + n = np.array(normal, dtype=float) + nn = np.linalg.norm(n) + if nn < 1e-9: + return base_hex + n /= nn + diff = max(0.0, float(np.dot(n, _LIGHT))) + intensity = ambient + (1 - ambient) * diff + + r = int(base_hex[1:3], 16) + g = int(base_hex[3:5], 16) + b = int(base_hex[5:7], 16) + r2 = min(255, int(r * intensity)) + g2 = min(255, int(g * intensity)) + b2 = min(255, int(b * intensity)) + return f"#{r2:02x}{g2:02x}{b2:02x}" + + +def hex_to_rgb(h: str) -> tuple: + h = h.lstrip("#") + return tuple(int(h[i:i+2], 16) for i in (0, 2, 4)) + + +# ───────────────────────────────────────────── +# Structures de données +# ───────────────────────────────────────────── + +@dataclass +class PaperSheet: + """ + Feuille de papier positionnée dans la scène. + + Paramètres + ---------- + svg_content : str + Contenu SVG brut (chaîne). Sera intégré tel quel dans le rendu. + width, height : float + Dimensions de la feuille en unités scène (mm conseillé). + position : tuple (x, y, z) + Centre de la feuille dans l'espace 3D. + rotation_x/y/z : float + Rotations en degrés autour des axes X, Y, Z (dans cet ordre). + paper_color : str + Couleur de fond de la feuille (blanc par défaut). + shadow : bool + Ajouter une légère ombre portée sous la feuille. + """ + svg_content: str + width: float = 80.0 + height: float = 110.0 + position: tuple = (0.0, 0.0, 0.0) + rotation_x: float = 0.0 + rotation_y: float = 0.0 + rotation_z: float = 0.0 + paper_color: str = "#FAFAF8" + shadow: bool = True + + def corners_3d(self) -> np.ndarray: + """Retourne les 4 coins de la feuille dans l'espace 3D.""" + hw, hh = self.width / 2, self.height / 2 + # Coins locaux : bas-gauche, bas-droite, haut-droite, haut-gauche + local = np.array([ + [-hw, -hh, 0], + [ hw, -hh, 0], + [ hw, hh, 0], + [-hw, hh, 0], + ], dtype=float) + # Rotations + for angle, axis in [ + (self.rotation_x, 'x'), + (self.rotation_y, 'y'), + (self.rotation_z, 'z'), + ]: + local = _rotate(local, angle, axis) + # Translation + px, py, pz = self.position + local += np.array([px, py, pz]) + return local + + def normal_3d(self) -> np.ndarray: + """Normale de la feuille après rotations.""" + n = np.array([0.0, 0.0, 1.0]) + n = _rotate(n[np.newaxis], self.rotation_x, 'x')[0] + n = _rotate(n[np.newaxis], self.rotation_y, 'y')[0] + n = _rotate(n[np.newaxis], self.rotation_z, 'z')[0] + return n + + +def _rotate(pts: np.ndarray, angle_deg: float, axis: str) -> np.ndarray: + if angle_deg == 0: + return pts + a = math.radians(angle_deg) + c, s = math.cos(a), math.sin(a) + if axis == 'x': + R = np.array([[1,0,0],[0,c,-s],[0,s,c]]) + elif axis == 'y': + R = np.array([[c,0,s],[0,1,0],[-s,0,c]]) + else: + R = np.array([[c,-s,0],[s,c,0],[0,0,1]]) + return (R @ pts.T).T + + +@dataclass +class SceneShape: + """Un solide build123d dans la scène.""" + shape: object # build123d Shape + base_color: str = "#5B8DB8" + position: tuple = (0.0, 0.0, 0.0) + rotation_x: float = 0.0 + rotation_y: float = 0.0 + rotation_z: float = 0.0 + tessellation_tolerance: float = 0.5 + + +@dataclass +class Scene: + """Conteneur de la scène.""" + _shapes: list = field(default_factory=list) + _papers: list = field(default_factory=list) + + def add_shape(self, shape, base_color: str = "#5B8DB8", + position=(0, 0, 0), + rotation_x=0.0, rotation_y=0.0, rotation_z=0.0, + tessellation_tolerance=0.5): + self._shapes.append(SceneShape( + shape=shape, + base_color=base_color, + position=position, + rotation_x=rotation_x, + rotation_y=rotation_y, + rotation_z=rotation_z, + tessellation_tolerance=tessellation_tolerance, + )) + + def add_paper(self, paper: PaperSheet): + self._papers.append(paper) + + +# ───────────────────────────────────────────── +# Extraction des polygones depuis build123d +# ───────────────────────────────────────────── + +def _extract_face_polygons(scene_shape: SceneShape): + """ + Retourne une liste de dicts : + { 'pts': np.ndarray (N,3), 'normal': np.ndarray (3,), 'color_base': str } + """ + shape = scene_shape.shape + px, py, pz = scene_shape.position + tol = scene_shape.tessellation_tolerance + polys = [] + + for face in shape.faces(): + verts, tris = face.tessellate(tol) + + # Normale de la face + try: + n = face.normal_at() + normal = np.array([n.X, n.Y, n.Z], dtype=float) + except Exception: + normal = np.array([0.0, 0.0, 1.0]) + + # Appliquer les rotations de l'objet + for angle, axis in [ + (scene_shape.rotation_x, 'x'), + (scene_shape.rotation_y, 'y'), + (scene_shape.rotation_z, 'z'), + ]: + normal = _rotate(normal[np.newaxis], angle, axis)[0] + + # Grouper les triangles en polygone (ici chaque tri est un poly) + for tri in tris: + pts = np.array([[verts[i].X, verts[i].Y, verts[i].Z] + for i in tri], dtype=float) + # Appliquer les rotations + for angle, axis in [ + (scene_shape.rotation_x, 'x'), + (scene_shape.rotation_y, 'y'), + (scene_shape.rotation_z, 'z'), + ]: + pts = _rotate(pts, angle, axis) + # Translation + pts += np.array([px, py, pz]) + + polys.append({ + 'pts': pts, + 'normal': normal, + 'color_base': scene_shape.base_color, + }) + + return polys + + +# ───────────────────────────────────────────── +# Painter's algorithm (tri par profondeur) +# ───────────────────────────────────────────── + +def _centroid_depth(pts: np.ndarray, M: np.ndarray) -> float: + """Profondeur moyenne d'un polygone projeté (composante Z de la vue).""" + # On utilise la 3ème ligne de la matrice complète Rx@Ry pour Z + return float(np.mean(pts[:, 2])) # approximation simple : Z monde + + +# ───────────────────────────────────────────── +# Manipulation SVG +# ───────────────────────────────────────────── + +def _parse_svg_viewbox(svg_str: str) -> Optional[tuple]: + """Extrait (min_x, min_y, width, height) depuis le viewBox.""" + m = re.search(r'viewBox=["\']([^"\']+)["\']', svg_str) + if m: + parts = re.split(r'[\s,]+', m.group(1).strip()) + if len(parts) == 4: + return tuple(float(p) for p in parts) + # Essai width/height + w = re.search(r']+width=["\']([0-9.]+)', svg_str) + h = re.search(r']+height=["\']([0-9.]+)', svg_str) + if w and h: + return (0, 0, float(w.group(1)), float(h.group(1))) + return None + + +def _svg_inner_content(svg_str: str) -> str: + """Extrait le contenu entre les balises ... .""" + m = re.search(r']*>(.*)', svg_str, re.DOTALL) + return m.group(1) if m else svg_str + + +def _strip_svg_defs_ids(content: str, prefix: str) -> str: + """Préfixe tous les ids pour éviter les collisions.""" + # Remplace id="xxx" et url(#xxx) avec un préfixe unique + content = re.sub(r'\bid="([^"]+)"', lambda m: f'id="{prefix}_{m.group(1)}"', content) + content = re.sub(r'url\(#([^)]+)\)', lambda m: f'url(#{prefix}_{m.group(1)})', content) + content = re.sub(r'href="#([^"]+)"', lambda m: f'href="#{prefix}_{m.group(1)}"', content) + return content + + +# ───────────────────────────────────────────── +# Renderer principal +# ───────────────────────────────────────────── + +class IsoRenderer: + """ + Renderer isométrique → SVG. + + Paramètres + ---------- + width, height : int + Dimensions du SVG de sortie en pixels. + iso_angle : float + Angle d'inclinaison isométrique (30° = iso classique, 35.26° = cavalière). + scale : float ou None + Échelle pixels/unité. Si None, ajustement automatique. + background : str ou None + Couleur de fond (ex: "#F0EDE8"). None = transparent. + stroke_width : float + Épaisseur des contours. + stroke_color : str + Couleur des contours. + """ + + def __init__(self, + width: int = 800, + height: int = 600, + iso_angle: float = 30.0, + scale: Optional[float] = None, + background: Optional[str] = "#F5F3EF", + stroke_width: float = 0.4, + stroke_color: str = "#1a1a1a"): + self.width = width + self.height = height + self.iso_angle = iso_angle + self.scale = scale + self.background = background + self.stroke_width = stroke_width + self.stroke_color = stroke_color + self.M = iso_matrix(iso_angle) + + def render(self, scene: Scene, output_path: str): + """Génère le fichier SVG.""" + + # 1. Collecter tous les polygones (shapes + papers) + all_polys = [] + + for ss in scene._shapes: + for poly in _extract_face_polygons(ss): + all_polys.append(('shape', poly, None)) + + for i, paper in enumerate(scene._papers): + corners = paper.corners_3d() + normal = paper.normal_3d() + all_polys.append(('paper', { + 'pts': corners, + 'normal': normal, + 'color_base': paper.paper_color, + }, (i, paper))) + + if not all_polys: + raise ValueError("La scène est vide.") + + # 2. Projeter tous les points pour calculer les bounds + all_pts_3d = np.vstack([p[1]['pts'] for p in all_polys]) + all_pts_2d = project(all_pts_3d, self.M) + + min_x, min_y = all_pts_2d.min(axis=0) + max_x, max_y = all_pts_2d.max(axis=0) + span_x = max_x - min_x or 1 + span_y = max_y - min_y or 1 + + padding = 40 + if self.scale is None: + scale = min( + (self.width - 2 * padding) / span_x, + (self.height - 2 * padding) / span_y, + ) + else: + scale = self.scale + + # Offset pour centrer + cx = (self.width - span_x * scale) / 2 - min_x * scale + cy = (self.height - span_y * scale) / 2 - min_y * scale + + def to_screen(pts_3d): + p2d = project(pts_3d, self.M) + sx = p2d[:, 0] * scale + cx + sy = p2d[:, 1] * scale + cy + # SVG : Y vers le bas, on inverse + sy = self.height - sy + return np.stack([sx, sy], axis=1) + + # 3. Trier par profondeur (painter's algorithm) + def depth_key(item): + _, poly, _ = item + return _centroid_depth(poly['pts'], self.M) + + all_polys.sort(key=depth_key) + + # 4. Construire le SVG + svg_lines = [ + f'', + ] + + # Defs (filtres shadow si nécessaire) + svg_lines.append('') + svg_lines.append( + '' + '' + '' + ) + # clipPath pour chaque feuille + for i, paper in enumerate(scene._papers): + corners_2d = to_screen(paper.corners_3d()) + pts_str = " ".join(f"{x:.2f},{y:.2f}" for x, y in corners_2d) + svg_lines.append( + f'' + f'' + f'' + ) + svg_lines.append('') + + # Fond + if self.background: + svg_lines.append( + f'' + ) + + # 5. Dessiner les polygones dans l'ordre + paper_indices_done = set() + + for kind, poly, extra in all_polys: + pts_2d = to_screen(poly['pts']) + pts_str = " ".join(f"{x:.2f},{y:.2f}" for x, y in pts_2d) + normal = poly['normal'] + + if kind == 'shape': + # Face 3D avec shading + # Backface culling léger : on garde toutes les faces visibles + color = shade_color(poly['color_base'], normal) + svg_lines.append( + f'' + ) + + elif kind == 'paper': + paper_idx, paper = extra + if paper_idx in paper_indices_done: + continue + paper_indices_done.add(paper_idx) + + corners_2d = to_screen(paper.corners_3d()) + pts_str_paper = " ".join(f"{x:.2f},{y:.2f}" for x, y in corners_2d) + + # Ombre portée + if paper.shadow: + svg_lines.append( + f'' + ) + + # Fond de la feuille (avec shading) + paper_color = shade_color(paper.paper_color, normal, ambient=0.7) + svg_lines.append( + f'' + ) + + # Contenu SVG de la feuille + self._embed_svg_on_paper( + svg_lines, paper, corners_2d, paper_idx + ) + + svg_lines.append('') + svg_str = "\n".join(svg_lines) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write(svg_str) + print(f"✓ Rendu sauvegardé : {output_path}") + + def _embed_svg_on_paper(self, svg_lines, paper, corners_2d, paper_idx): + """ + Intègre le contenu SVG de la feuille en appliquant + une transformation perspective affine (homographie 2D approximée + par une transformation affine sur les 3 premiers coins). + """ + vb = _parse_svg_viewbox(paper.svg_content) + if vb is None: + # Fallback : pas de viewBox trouvé, on essaie quand même + vb = (0, 0, paper.width, paper.height) + + vb_x, vb_y, vb_w, vb_h = vb + + # Coins de destination dans l'écran (ordre : BG, BD, HD, HG) + dst = corners_2d # shape (4,2) + + # Coins source dans l'espace SVG original + src = np.array([ + [vb_x, vb_y + vb_h], # bas-gauche + [vb_x + vb_w, vb_y + vb_h], # bas-droite + [vb_x + vb_w, vb_y], # haut-droite + [vb_x, vb_y], # haut-gauche + ], dtype=float) + + # Transformation affine approchée (3 points) + # On résout : dst[0:3] = A @ src[0:3] (en coordonnées homogènes) + S = np.array([ + [src[0,0], src[0,1], 1, 0, 0, 0], + [0, 0, 0, src[0,0], src[0,1], 1], + [src[1,0], src[1,1], 1, 0, 0, 0], + [0, 0, 0, src[1,0], src[1,1], 1], + [src[2,0], src[2,1], 1, 0, 0, 0], + [0, 0, 0, src[2,0], src[2,1], 1], + ], dtype=float) + D = np.array([ + dst[0,0], dst[0,1], + dst[1,0], dst[1,1], + dst[2,0], dst[2,1], + ], dtype=float) + + try: + params = np.linalg.solve(S, D) + a, b, c, d, e, f_ = params + # Matrice affine SVG : matrix(a,d,b,e,c,f) + transform = f"matrix({a:.6f},{d:.6f},{b:.6f},{e:.6f},{c:.6f},{f_:.6f})" + except np.linalg.LinAlgError: + # Fallback trivial + transform = "matrix(1,0,0,1,0,0)" + + # Préfixer les IDs pour éviter les collisions + prefix = f"p{paper_idx}" + inner = _svg_inner_content(paper.svg_content) + inner = _strip_svg_defs_ids(inner, prefix) + + pts_clip = " ".join(f"{x:.2f},{y:.2f}" for x, y in corners_2d) + + svg_lines.append( + f'' + f'' + + inner + + f'' + ) diff --git a/ocp_screenshot.py b/ocp_screenshot.py new file mode 100644 index 0000000..82b90f4 --- /dev/null +++ b/ocp_screenshot.py @@ -0,0 +1,280 @@ +""" +ocp_screenshot.py +================= +Screenshot d'un modèle build123d via OCP (OCCT), fond transparent. + +Dépendances : build123d, Pillow, numpy +Nécessite : Xvfb (sudo apt install xvfb) + +Lancement +--------- + # Directement (si DISPLAY est déjà dispo) : + python ocp_screenshot.py + + # Headless (sans serveur X) : + xvfb-run -a python ocp_screenshot.py + +API +--- + from ocp_screenshot import screenshot + + screenshot( + shape, # build123d Shape / Compound / Part + "output.png", + width=800, height=600, + orientation="ZupRight", # voir ORIENTATIONS ci-dessous + fit_padding=0.05, # marge autour du modèle (0–1) + background=None, # None = transparent, ou "#RRGGBB" + color="#A8C4DC", # couleur du solide + line_color="#1a1a1a", # couleur des arêtes + show_edges=True, + ) + +Orientations disponibles +------------------------ + ZupRight → isométrique Z-haut, côté droit (défaut, standard build123d) + ZupLeft → isométrique Z-haut, côté gauche + YupRight → isométrique Y-haut, côté droit + YupLeft → isométrique Y-haut, côté gauche + Top / Bottom / Front / Back / Left / Right +""" + +import subprocess, sys, os, tempfile +import numpy as np +from PIL import Image + +# ── Orientations ──────────────────────────────────────────────────────────── +from OCP.V3d import V3d_TypeOfOrientation as _Ori + +ORIENTATIONS = { + "ZupRight" : _Ori.V3d_TypeOfOrientation_Zup_AxoRight, + "ZupLeft" : _Ori.V3d_TypeOfOrientation_Zup_AxoLeft, + "YupRight" : _Ori.V3d_TypeOfOrientation_Yup_AxoRight, + "YupLeft" : _Ori.V3d_TypeOfOrientation_Yup_AxoLeft, + "Top" : _Ori.V3d_Zpos, + "Bottom" : _Ori.V3d_Zneg, + "Front" : _Ori.V3d_Xpos, + "Back" : _Ori.V3d_Xneg, + "Left" : _Ori.V3d_Ypos, + "Right" : _Ori.V3d_Yneg, +} + + +def _hex_to_occ_color(hex_str: str): + """Convertit '#RRGGBB' en Quantity_Color OCC.""" + from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB + h = hex_str.lstrip("#") + r, g, b = (int(h[i:i+2], 16) / 255.0 for i in (0, 2, 4)) + return Quantity_Color(r, g, b, Quantity_TOC_RGB) + + +_OCC_SESSION: dict = {} # conn + driver + viewer réutilisés dans le même process + + +def _make_viewer(width: int, height: int): + """ + Crée (ou réutilise) un viewer OCC offscreen. + + Aspect_DisplayConnection ne peut être instancié qu'une fois par process + sous Xvfb — on conserve donc la connexion et le driver en cache de module. + Chaque appel crée en revanche une nouvelle View (fenêtre offscreen). + """ + from OCP.Aspect import Aspect_DisplayConnection + from OCP.OpenGl import OpenGl_GraphicDriver + from OCP.V3d import V3d_Viewer + from OCP.Xw import Xw_Window + + if "viewer" not in _OCC_SESSION: + conn = Aspect_DisplayConnection() + driver = OpenGl_GraphicDriver(conn, False) + viewer = V3d_Viewer(driver) + viewer.SetDefaultLights() + viewer.SetLightOn() + _OCC_SESSION["conn"] = conn + _OCC_SESSION["driver"] = driver + _OCC_SESSION["viewer"] = viewer + + viewer = _OCC_SESSION["viewer"] + conn = _OCC_SESSION["conn"] + + view = viewer.CreateView() + win = Xw_Window(conn, "ocp_screenshot", 0, 0, width, height) + view.SetWindow(win) + win.Map() + + return viewer, view + + +def _render_to_array(view, shape_occ, ctx, width: int, height: int, + bg_color, orientation, fit_padding: float) -> np.ndarray: + """Configure la vue, lance le rendu, retourne un array HxWx3 uint8.""" + from OCP.Image import Image_AlienPixMap + from OCP.TCollection import TCollection_AsciiString + from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB + + view.SetBackgroundColor(bg_color) + view.SetProj(orientation) + view.FitAll(fit_padding) + view.Redraw() + + pix = Image_AlienPixMap() + view.ToPixMap(pix, width, height) + + # Sauvegarde temporaire puis lecture Pillow + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f: + tmp = f.name + pix.Save(TCollection_AsciiString(tmp)) + arr = np.array(Image.open(tmp).convert("RGB"), dtype=np.float32) + os.unlink(tmp) + return arr + + +def _double_render_transparent(view, width, height, orientation, + fit_padding) -> np.ndarray: + """ + Technique double-render blanc/noir → canal alpha propre. + + Principe : sur fond blanc Cw = C·α + 1·(1−α) = C·α + 1 − α + sur fond noir Cb = C·α + donc α = 1 − (Cw − Cb) et C = Cb / α + """ + from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB + + white = Quantity_Color(1.0, 1.0, 1.0, Quantity_TOC_RGB) + black = Quantity_Color(0.0, 0.0, 0.0, Quantity_TOC_RGB) + + w = _render_to_array(view, None, None, width, height, + white, orientation, fit_padding) + b = _render_to_array(view, None, None, width, height, + black, orientation, fit_padding) + + # Alpha (par canal, on prend le max pour être robuste aux artefacts) + alpha = 1.0 - (w - b) / 255.0 + alpha = np.clip(alpha.max(axis=2), 0.0, 1.0) + + # Couleur pré-multipliée → non-pré-multipliée + a3 = alpha[..., np.newaxis] + rgb = np.where(a3 > 0.01, b / np.maximum(a3, 0.01), 0.0) + rgb = np.clip(rgb, 0, 255).astype(np.uint8) + + return np.dstack([rgb, (alpha * 255).astype(np.uint8)]) # RGBA + + +def screenshot( + shape, + output_path: str, + width: int = 800, + height: int = 600, + orientation: str = "ZupRight", + fit_padding: float = 0.05, + background: "str | None" = None, + color: str = "#A8C4DC", + line_color: str = "#1a1a1a", + show_edges: bool = True, +): + """ + Génère un screenshot PNG du modèle build123d. + + Parameters + ---------- + shape : build123d Shape (Solid, Compound, Part…) + output_path : chemin de sortie (.png) + width/height : dimensions en pixels + orientation : voir ORIENTATIONS (défaut "ZupRight") + fit_padding : marge relative autour du modèle (0.05 = 5 %) + background : None → fond transparent | "#RRGGBB" → fond coloré + color : couleur du solide (hex) + line_color : couleur des arêtes (hex) + show_edges : afficher les arêtes + """ + from OCP.AIS import AIS_InteractiveContext, AIS_Shape + from OCP.Prs3d import Prs3d_Drawer + from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB + from OCP.Aspect import Aspect_TypeOfLine + + if orientation not in ORIENTATIONS: + raise ValueError( + f"Orientation '{orientation}' inconnue. " + f"Choisir parmi : {list(ORIENTATIONS.keys())}" + ) + ori = ORIENTATIONS[orientation] + + # Extraire le TopoDS_Shape depuis build123d + occ_shape = shape.wrapped + + viewer, view = _make_viewer(width, height) + ctx = AIS_InteractiveContext(viewer) + + # Couleur du solide + ais = AIS_Shape(occ_shape) + ais.SetColor(_hex_to_occ_color(color)) + ais.SetTransparency(0.0) + + if show_edges: + # Configurer les arêtes dans le drawer + drawer = ais.Attributes() + drawer.SetFaceBoundaryDraw(True) + drawer.FaceBoundaryAspect().SetColor(_hex_to_occ_color(line_color)) + drawer.FaceBoundaryAspect().SetWidth(1.0) + + ctx.Display(ais, True) + view.SetProj(ori) + view.FitAll(fit_padding) + + if background is None: + # Fond transparent via double-render + rgba = _double_render_transparent(view, width, height, ori, fit_padding) + Image.fromarray(rgba, "RGBA").save(output_path) + else: + # Fond coloré direct + from OCP.Image import Image_AlienPixMap + from OCP.TCollection import TCollection_AsciiString + view.SetBackgroundColor(_hex_to_occ_color(background)) + view.Redraw() + pix = Image_AlienPixMap() + view.ToPixMap(pix, width, height) + pix.Save(TCollection_AsciiString(output_path)) + + print(f"✓ {output_path} ({width}×{height}px, orientation={orientation})") + + +# ───────────────────────────────────────────── +# Exemple d'utilisation +# ───────────────────────────────────────────── +if __name__ == "__main__": + from build123d import * + + # Modèle simple : boîte + cylindre (Compound) + with BuildPart() as p: + Box(40, 30, 15) + with Locations((0, 0, 15)): + Cylinder(8, 20) + + # Fond transparent + screenshot( + p.part, + "model_transparent.png", + width=800, height=600, + orientation="ZupRight", + color="#5B8DB8", + ) + + # Fond coloré + screenshot( + p.part, + "model_bg.png", + width=800, height=600, + orientation="ZupRight", + background="#F0EDE6", + color="#5B8DB8", + ) + + # Quelques orientations + for ori in ["Top", "Front", "ZupLeft"]: + screenshot( + p.part, + f"model_{ori.lower()}.png", + width=600, height=450, + orientation=ori, + color="#C0392B", + ) diff --git a/poignee.ipynb b/poignee.ipynb new file mode 100644 index 0000000..2bab361 --- /dev/null +++ b/poignee.ipynb @@ -0,0 +1,312 @@ +{ + "cells": [ + { + "cell_type": "code", + "id": "initial_id", + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2026-05-24T17:45:15.002996810Z", + "start_time": "2026-05-24T17:45:13.605491729Z" + } + }, + "source": [ + "from build123d import *\n", + "from ocp_vscode import show, set_port\n", + "\n", + "### Poignée\n", + "\n", + "trou_pivot_diam = 4 + 0.4\n", + "trou_pivot_diam_ext = 8\n", + "\n", + "pt_contact_dist_trou = 36\n", + "pt_contact_epaisseur = 2.5\n", + "\n", + "ecart_trou_epaulement = 6.5\n", + "\n", + "corps_largueur = 18\n", + "dist_pivot_epaulement_plateau = 17\n", + "dist_pivot_bord_plateau = 5\n", + "\n", + "plateau_ecart_trou_z = 30\n", + "plateau_longueur = 110\n", + "plateau_largueur = 50\n", + "plateau_epaisseur = 5\n", + "\n", + "vis_couplage_diam_pas = 2.8\n", + "vis_couplage_diam = 4\n", + "vis_couplage_diam_trou = 8\n", + "\n", + "with BuildPart() as bp:\n", + " with BuildSketch(Plane.XZ):\n", + " with BuildLine() as bl:\n", + " l1 = ThreePointArc((-trou_pivot_diam_ext / 2 -1, 0),\n", + " (0, -trou_pivot_diam_ext / 2),\n", + " (trou_pivot_diam_ext / 2, 0))\n", + " l2 = Line(l1 @ 1, l1 @ 1 + (0, 6.5))\n", + " l3 = Line(l2 @ 1, l2 @ 1 + (pt_contact_dist_trou - trou_pivot_diam / 2, 0))\n", + " l4 = SagittaArc(l3 @ 1,\n", + " (dist_pivot_bord_plateau + plateau_longueur, plateau_ecart_trou_z - 2),\n", + " 5)\n", + "\n", + " l4_bis = Line(l4 @ 1, l4 @ 1 + (0, 4))\n", + " l5 = Line(l4_bis @ 1, (dist_pivot_epaulement_plateau, plateau_ecart_trou_z))\n", + " l7 = Line(l1 @ 0 + (0, 15), l1 @ 0)\n", + " l6 = SagittaArc(l5 @ 1, l7 @ 0, 2)\n", + " make_face()\n", + " corners = [vertices().filter_by_position(Axis.Y, minimum=6.5 - 0.1, maximum=6.5 + 0.1),\n", + " vertices().filter_by_position(Axis.Y, minimum=15 - 0.1, maximum=15 + 0.1)]\n", + " fillet(corners, radius=2)\n", + " with Locations(l3 @ 1):\n", + " Circle(pt_contact_epaisseur)\n", + " fillet(vertices(Select.LAST), radius=3)\n", + " Circle(trou_pivot_diam / 2, mode=Mode.SUBTRACT)\n", + " extrude(amount=corps_largueur / 2, both=True)\n", + "\n", + " with BuildSketch(Plane.XY.offset(plateau_ecart_trou_z)):\n", + " with Locations((dist_pivot_bord_plateau, 0)):\n", + " Rectangle(plateau_longueur, plateau_largueur,\n", + " align=(Align.MIN, Align.CENTER))\n", + " plateau = extrude(amount=plateau_epaisseur)\n", + " side_edges = edges(Select.LAST).filter_by(lambda x: not x.is_interior)\n", + " chamfer(side_edges, length=1.5)\n", + "\n", + " top_face = faces().filter_by(Plane.XY).sort_by(Axis.Z)[-1]\n", + " with Locations(top_face):\n", + " with Locations((-10, 0), (-35, 0)):\n", + " CounterSinkHole(radius=vis_couplage_diam_pas / 2, counter_sink_radius=vis_couplage_diam_trou / 2, depth=22)\n", + "\n", + " logo_shapes = import_svg(\"logo.svg\")\n", + " logo_emboss = Compound(logo_shapes)\n", + " with BuildSketch(top_face):\n", + " with Locations((33, 0)):\n", + " add(logo_emboss.translate(-logo_emboss.center() + (3, 0, 0)).rotate(Axis.Z, -90))\n", + " scale(by=0.8)\n", + " extrude(amount=0.6)\n", + "\n", + " face_int_pivot = faces().filter_by(Plane.YZ).filter_by_position(Axis.X,\n", + " minimum=trou_pivot_diam_ext / 2 - 0.001,\n", + " maximum=trou_pivot_diam_ext / 2 + 0.001)[0]\n", + " with BuildSketch(face_int_pivot):\n", + " with Locations((2,0)):\n", + " Rectangle(15, corps_largueur-3, align=(Align.MAX, Align.CENTER))\n", + " extrude(amount=-1, mode=Mode.SUBTRACT)\n", + "\n", + "plateau, corps = split(bp.part,\n", + " bisect_by=Plane.XY.offset(plateau_ecart_trou_z),\n", + " keep=Keep.BOTH)\n", + "compound = Compound(children=[corps, plateau])\n", + "set_port(3939)\n", + "show(compound)\n", + "export_step(compound, \"/tmp/poignee.step\")\n", + "export_step(corps, \"/tmp/corps_poignee.step\")\n", + "export_step(plateau, \"/tmp/plateau_poignee.step\")" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "c+\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 147, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 147 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-05-24T15:42:01.875476789Z", + "start_time": "2026-05-24T15:42:01.834556919Z" + } + }, + "cell_type": "code", + "source": [ + "with BuildPart() as bp:\n", + " Box(10,10,5)\n", + " Hole(trou_pivot_diam/2, 10)\n", + "\n", + "export_step(bp.part, \"/tmp/test_trou_pivot.step\")\n", + "bp.part\n" + ], + "id": "d1d89625781113aa", + "outputs": [ + { + "data": { + "text/plain": [ + "Part at 0x7e1c23aeced0, label(), #children(0)" + ], + "text/html": [ + "
" + ] + }, + "execution_count": 139, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 139 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/raiser.ipynb b/raiser.ipynb new file mode 100644 index 0000000..81be6de --- /dev/null +++ b/raiser.ipynb @@ -0,0 +1,84 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "initial_id", + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "from build123d import *\n", + "from ocp_vscode import show, set_port\n", + "\n", + "pattes_largueur = 5\n", + "pattes_profondeur = 5\n", + "bras_patte_dist = 8\n", + "bloc_hauteur_totale = 10\n", + "bloc_hauteur = 6\n", + "pourtour_hauteur = bloc_hauteur_totale - bloc_hauteur\n", + "raiser_dim_x = 30\n", + "raiser_dim_y = 18\n", + "raiser_dim_z = 15\n", + "cav_bloc_dim_x = 24\n", + "cav_bloc_dim_y = 14\n", + "\n", + "with BuildPart() as bp:\n", + " raiser = Box(raiser_dim_x, raiser_dim_y, raiser_dim_z)\n", + " raiser_faces = raiser.faces()\n", + " bottom_face = faces().filter_by(Plane.top).sort_by(Axis.Z)[0]\n", + " with BuildSketch(Plane(bottom_face).reverse()):\n", + " Rectangle(cav_bloc_dim_x, cav_bloc_dim_y)\n", + " extrude(amount=(bloc_hauteur + 1), mode=Mode.SUBTRACT)\n", + "\n", + " for face in raiser_faces.filter_by(lambda f: abs(f.normal_at().Z) < 0.01):\n", + " plane = Plane(face)\n", + " offset_plane = plane.offset(bras_patte_dist)\n", + " with BuildSketch(offset_plane):\n", + " with Locations((bottom_face.center().Z,)):\n", + " Rectangle(2, 5, align=(Align.MIN, Align.CENTER))\n", + " with BuildSketch(face):\n", + " add(face.rotate(Axis.Y, 90)) # ← utilise directement la face comme sketch\n", + " loft()\n", + "\n", + " patte_plane = Plane(\n", + " origin=offset_plane.origin,\n", + " x_dir=-offset_plane.y_dir,\n", + " z_dir=(0, 0, 1)\n", + " ).offset(bottom_face.center().Z)\n", + " with BuildSketch(patte_plane):\n", + " Rectangle(5, bras_patte_dist - 1, align=(Align.CENTER, Align.MIN))\n", + " with BuildSketch(patte_plane.offset(-pourtour_hauteur)):\n", + " Rectangle(5, 5, align=(Align.CENTER, Align.MIN))\n", + "\n", + " loft()\n", + "\n", + "set_port(3939)\n", + "show(bp.part)\n", + "export_step(bp.part, \"/tmp/raiser.step\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/test_render.ipynb b/test_render.ipynb new file mode 100644 index 0000000..3b58661 --- /dev/null +++ b/test_render.ipynb @@ -0,0 +1,332 @@ +{ + "cells": [ + { + "cell_type": "code", + "id": "initial_id", + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2026-05-25T22:35:40.534794354Z", + "start_time": "2026-05-25T22:35:37.180066505Z" + } + }, + "source": [ + "\"\"\"\n", + "ocp_screenshot.py\n", + "=================\n", + "Screenshot d'un modèle build123d via OCP (OCCT), fond transparent.\n", + "\n", + "Dépendances : build123d, Pillow, numpy\n", + "Nécessite : Xvfb (sudo apt install xvfb)\n", + "\n", + "Lancement\n", + "---------\n", + " # Directement (si DISPLAY est déjà dispo) :\n", + " python ocp_screenshot.py\n", + "\n", + " # Headless (sans serveur X) :\n", + " xvfb-run -a python ocp_screenshot.py\n", + "\n", + "API\n", + "---\n", + " from ocp_screenshot import screenshot\n", + "\n", + " screenshot(\n", + " shape, # build123d Shape / Compound / Part\n", + " \"output.png\",\n", + " width=800, height=600,\n", + " orientation=\"ZupRight\", # voir ORIENTATIONS ci-dessous\n", + " fit_padding=0.05, # marge autour du modèle (0–1)\n", + " background=None, # None = transparent, ou \"#RRGGBB\"\n", + " color=\"#A8C4DC\", # couleur du solide\n", + " line_color=\"#1a1a1a\", # couleur des arêtes\n", + " show_edges=True,\n", + " )\n", + "\n", + "Orientations disponibles\n", + "------------------------\n", + " ZupRight → isométrique Z-haut, côté droit (défaut, standard build123d)\n", + " ZupLeft → isométrique Z-haut, côté gauche\n", + " YupRight → isométrique Y-haut, côté droit\n", + " YupLeft → isométrique Y-haut, côté gauche\n", + " Top / Bottom / Front / Back / Left / Right\n", + "\"\"\"\n", + "\n", + "import subprocess, sys, os, tempfile\n", + "import numpy as np\n", + "from PIL import Image\n", + "\n", + "# ── Orientations ────────────────────────────────────────────────────────────\n", + "from OCP.V3d import V3d_TypeOfOrientation as _Ori\n", + "\n", + "ORIENTATIONS = {\n", + " \"ZupRight\" : _Ori.V3d_TypeOfOrientation_Zup_AxoRight,\n", + " \"ZupLeft\" : _Ori.V3d_TypeOfOrientation_Zup_AxoLeft,\n", + " \"YupRight\" : _Ori.V3d_TypeOfOrientation_Yup_AxoRight,\n", + " \"YupLeft\" : _Ori.V3d_TypeOfOrientation_Yup_AxoLeft,\n", + " \"Top\" : _Ori.V3d_Zpos,\n", + " \"Bottom\" : _Ori.V3d_Zneg,\n", + " \"Front\" : _Ori.V3d_Xpos,\n", + " \"Back\" : _Ori.V3d_Xneg,\n", + " \"Left\" : _Ori.V3d_Ypos,\n", + " \"Right\" : _Ori.V3d_Yneg,\n", + "}\n", + "\n", + "\n", + "def _hex_to_occ_color(hex_str: str):\n", + " \"\"\"Convertit '#RRGGBB' en Quantity_Color OCC.\"\"\"\n", + " from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB\n", + " h = hex_str.lstrip(\"#\")\n", + " r, g, b = (int(h[i:i+2], 16) / 255.0 for i in (0, 2, 4))\n", + " return Quantity_Color(r, g, b, Quantity_TOC_RGB)\n", + "\n", + "\n", + "_OCC_SESSION: dict = {} # conn + driver + viewer réutilisés dans le même process\n", + "\n", + "\n", + "def _make_viewer(width: int, height: int):\n", + " \"\"\"\n", + " Crée (ou réutilise) un viewer OCC offscreen.\n", + "\n", + " Aspect_DisplayConnection ne peut être instancié qu'une fois par process\n", + " sous Xvfb — on conserve donc la connexion et le driver en cache de module.\n", + " Chaque appel crée en revanche une nouvelle View (fenêtre offscreen).\n", + " \"\"\"\n", + " from OCP.Aspect import Aspect_DisplayConnection\n", + " from OCP.OpenGl import OpenGl_GraphicDriver\n", + " from OCP.V3d import V3d_Viewer\n", + " from OCP.Xw import Xw_Window\n", + "\n", + " if \"viewer\" not in _OCC_SESSION:\n", + " conn = Aspect_DisplayConnection()\n", + " driver = OpenGl_GraphicDriver(conn, False)\n", + " viewer = V3d_Viewer(driver)\n", + " viewer.SetDefaultLights()\n", + " viewer.SetLightOn()\n", + " _OCC_SESSION[\"conn\"] = conn\n", + " _OCC_SESSION[\"driver\"] = driver\n", + " _OCC_SESSION[\"viewer\"] = viewer\n", + "\n", + " viewer = _OCC_SESSION[\"viewer\"]\n", + " conn = _OCC_SESSION[\"conn\"]\n", + "\n", + " view = viewer.CreateView()\n", + " win = Xw_Window(conn, \"ocp_screenshot\", 0, 0, width, height)\n", + " view.SetWindow(win)\n", + " win.Map()\n", + "\n", + " return viewer, view\n", + "\n", + "\n", + "def _render_to_array(view, shape_occ, ctx, width: int, height: int,\n", + " bg_color, orientation, fit_padding: float) -> np.ndarray:\n", + " \"\"\"Configure la vue, lance le rendu, retourne un array HxWx3 uint8.\"\"\"\n", + " from OCP.Image import Image_AlienPixMap\n", + " from OCP.TCollection import TCollection_AsciiString\n", + " from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB\n", + "\n", + " view.SetBackgroundColor(bg_color)\n", + " view.SetProj(orientation)\n", + " view.FitAll(fit_padding)\n", + " view.Redraw()\n", + "\n", + " pix = Image_AlienPixMap()\n", + " view.ToPixMap(pix, width, height)\n", + "\n", + " # Sauvegarde temporaire puis lecture Pillow\n", + " with tempfile.NamedTemporaryFile(suffix=\".png\", delete=False) as f:\n", + " tmp = f.name\n", + " pix.Save(TCollection_AsciiString(tmp))\n", + " arr = np.array(Image.open(tmp).convert(\"RGB\"), dtype=np.float32)\n", + " os.unlink(tmp)\n", + " return arr\n", + "\n", + "\n", + "def _double_render_transparent(view, width, height, orientation,\n", + " fit_padding) -> np.ndarray:\n", + " \"\"\"\n", + " Technique double-render blanc/noir → canal alpha propre.\n", + "\n", + " Principe : sur fond blanc Cw = C·α + 1·(1−α) = C·α + 1 − α\n", + " sur fond noir Cb = C·α\n", + " donc α = 1 − (Cw − Cb) et C = Cb / α\n", + " \"\"\"\n", + " from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB\n", + "\n", + " white = Quantity_Color(1.0, 1.0, 1.0, Quantity_TOC_RGB)\n", + " black = Quantity_Color(0.0, 0.0, 0.0, Quantity_TOC_RGB)\n", + "\n", + " w = _render_to_array(view, None, None, width, height,\n", + " white, orientation, fit_padding)\n", + " b = _render_to_array(view, None, None, width, height,\n", + " black, orientation, fit_padding)\n", + "\n", + " # Alpha (par canal, on prend le max pour être robuste aux artefacts)\n", + " alpha = 1.0 - (w - b) / 255.0\n", + " alpha = np.clip(alpha.max(axis=2), 0.0, 1.0)\n", + "\n", + " # Couleur pré-multipliée → non-pré-multipliée\n", + " a3 = alpha[..., np.newaxis]\n", + " rgb = np.where(a3 > 0.01, b / np.maximum(a3, 0.01), 0.0)\n", + " rgb = np.clip(rgb, 0, 255).astype(np.uint8)\n", + "\n", + " return np.dstack([rgb, (alpha * 255).astype(np.uint8)]) # RGBA\n", + "\n", + "\n", + "def screenshot(\n", + " shape,\n", + " output_path: str,\n", + " width: int = 800,\n", + " height: int = 600,\n", + " orientation: str = \"ZupRight\",\n", + " fit_padding: float = 0.05,\n", + " background: \"str | None\" = None,\n", + " color: str = \"#A8C4DC\",\n", + " line_color: str = \"#1a1a1a\",\n", + " show_edges: bool = True,\n", + "):\n", + " \"\"\"\n", + " Génère un screenshot PNG du modèle build123d.\n", + "\n", + " Parameters\n", + " ----------\n", + " shape : build123d Shape (Solid, Compound, Part…)\n", + " output_path : chemin de sortie (.png)\n", + " width/height : dimensions en pixels\n", + " orientation : voir ORIENTATIONS (défaut \"ZupRight\")\n", + " fit_padding : marge relative autour du modèle (0.05 = 5 %)\n", + " background : None → fond transparent | \"#RRGGBB\" → fond coloré\n", + " color : couleur du solide (hex)\n", + " line_color : couleur des arêtes (hex)\n", + " show_edges : afficher les arêtes\n", + " \"\"\"\n", + " from OCP.AIS import AIS_InteractiveContext, AIS_Shape\n", + " from OCP.Prs3d import Prs3d_Drawer\n", + " from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB\n", + " from OCP.Aspect import Aspect_TypeOfLine\n", + "\n", + " if orientation not in ORIENTATIONS:\n", + " raise ValueError(\n", + " f\"Orientation '{orientation}' inconnue. \"\n", + " f\"Choisir parmi : {list(ORIENTATIONS.keys())}\"\n", + " )\n", + " ori = ORIENTATIONS[orientation]\n", + "\n", + " # Extraire le TopoDS_Shape depuis build123d\n", + " occ_shape = shape.wrapped\n", + "\n", + " viewer, view = _make_viewer(width, height)\n", + " ctx = AIS_InteractiveContext(viewer)\n", + "\n", + " # Couleur du solide\n", + " ais = AIS_Shape(occ_shape)\n", + " ais.SetColor(_hex_to_occ_color(color))\n", + " ais.SetTransparency(0.0)\n", + "\n", + " if show_edges:\n", + " # Configurer les arêtes dans le drawer\n", + " drawer = ais.Attributes()\n", + " drawer.SetFaceBoundaryDraw(True)\n", + " drawer.FaceBoundaryAspect().SetColor(_hex_to_occ_color(line_color))\n", + " drawer.FaceBoundaryAspect().SetWidth(1.0)\n", + "\n", + " ctx.Display(ais, True)\n", + " view.SetProj(ori)\n", + " view.FitAll(fit_padding)\n", + "\n", + " if background is None:\n", + " # Fond transparent via double-render\n", + " rgba = _double_render_transparent(view, width, height, ori, fit_padding)\n", + " Image.fromarray(rgba, \"RGBA\").save(output_path)\n", + " else:\n", + " # Fond coloré direct\n", + " from OCP.Image import Image_AlienPixMap\n", + " from OCP.TCollection import TCollection_AsciiString\n", + " view.SetBackgroundColor(_hex_to_occ_color(background))\n", + " view.Redraw()\n", + " pix = Image_AlienPixMap()\n", + " view.ToPixMap(pix, width, height)\n", + " pix.Save(TCollection_AsciiString(output_path))\n", + "\n", + " print(f\"✓ {output_path} ({width}×{height}px, orientation={orientation})\")\n", + "\n", + "\n", + "# ─────────────────────────────────────────────\n", + "# Exemple d'utilisation\n", + "# ─────────────────────────────────────────────\n", + "if __name__ == \"__main__\":\n", + " from build123d import *\n", + "\n", + " # Modèle simple : boîte + cylindre (Compound)\n", + " with BuildPart() as p:\n", + " Box(40, 30, 15)\n", + " with Locations((0, 0, 15)):\n", + " Cylinder(8, 20)\n", + "\n", + " # Fond transparent\n", + " screenshot(\n", + " p.part,\n", + " \"model_transparent.png\",\n", + " width=800, height=600,\n", + " orientation=\"ZupRight\",\n", + " color=\"#5B8DB8\",\n", + " )\n", + "\n", + " # Fond coloré\n", + " screenshot(\n", + " p.part,\n", + " \"model_bg.png\",\n", + " width=800, height=600,\n", + " orientation=\"ZupRight\",\n", + " background=\"#F0EDE6\",\n", + " color=\"#5B8DB8\",\n", + " )\n", + "\n", + " # Quelques orientations\n", + " for ori in [\"Top\", \"Front\", \"ZupLeft\"]:\n", + " screenshot(\n", + " p.part,\n", + " f\"model_{ori.lower()}.png\",\n", + " width=600, height=450,\n", + " orientation=ori,\n", + " color=\"#C0392B\",\n", + " )" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ model_transparent.png (800×600px, orientation=ZupRight)\n", + "✓ model_bg.png (800×600px, orientation=ZupRight)\n", + "✓ model_top.png (600×450px, orientation=Top)\n", + "✓ model_front.png (600×450px, orientation=Front)\n", + "✓ model_zupleft.png (600×450px, orientation=ZupLeft)\n" + ] + } + ], + "execution_count": 1 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tractocadre.ipynb b/tractocadre.ipynb index be81fc0..aced931 100644 --- a/tractocadre.ipynb +++ b/tractocadre.ipynb @@ -3,8 +3,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-05-22T19:37:45.609594692Z", - "start_time": "2026-05-22T19:37:43.862365665Z" + "end_time": "2026-05-23T01:58:20.511642454Z", + "start_time": "2026-05-23T01:58:19.038575293Z" } }, "cell_type": "code", @@ -126,7 +126,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "c++\n" + "ccc\n" ] }, { @@ -135,12 +135,12 @@ "True" ] }, - "execution_count": 145, + "execution_count": 146, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 145 + "execution_count": 146 }, { "metadata": { diff --git a/tractrudeuse_v2.ipynb b/tractrudeuse_v2.ipynb index e1fe861..ec93372 100644 --- a/tractrudeuse_v2.ipynb +++ b/tractrudeuse_v2.ipynb @@ -6,8 +6,8 @@ "metadata": { "collapsed": true, "ExecuteTime": { - "end_time": "2026-05-20T01:46:48.635159570Z", - "start_time": "2026-05-20T01:46:33.769195980Z" + "end_time": "2026-05-25T22:39:19.639342988Z", + "start_time": "2026-05-25T22:39:17.795171555Z" } }, "source": [ @@ -490,51 +490,22 @@ "\n", "gap = 50\n", "\n", - "show(\n", - " make_bloc_extrudeur(),\n", - " make_bac_recup().translate((0, 0, -(bac_hauteur / 2 + bloc_extrudeur_ext_z / 2) + emboitement_profondeur)),\n", - " make_couvercle().translate((0, 0, couv_ext_z / 2 + bloc_extrudeur_ext_z / 2 - emboitement_profondeur)),\n", - " names=[\"superieure\", \"inferieure\", \"couvercle\"],\n", - " colors=[\"#ff7800\", \"#000000\", \"#000000\"],\n", - ")" + "# show(\n", + "# make_bloc_extrudeur(),\n", + "# make_bac_recup().translate((0, 0, -(bac_hauteur / 2 + bloc_extrudeur_ext_z / 2) + emboitement_profondeur)),\n", + "# make_couvercle().translate((0, 0, couv_ext_z / 2 + bloc_extrudeur_ext_z / 2 - emboitement_profondeur)),\n", + "# names=[\"superieure\", \"inferieure\", \"couvercle\"],\n", + "# colors=[\"#ff7800\", \"#000000\", \"#000000\"],\n", + "# )" ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "UserWarning: Unknown collapse value from viewer: none\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+c+\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "CameraWarning: Object may extend beyond view (484.5% of previous size). Skip warnings with `ignore_camera_warnings()`\n" - ] - } - ], - "execution_count": 10 + "outputs": [], + "execution_count": 4 }, { "metadata": { "ExecuteTime": { - "end_time": "2026-05-18T23:18:32.922490156Z", - "start_time": "2026-05-18T23:18:32.423433469Z" + "end_time": "2026-05-25T22:38:21.604563409Z", + "start_time": "2026-05-25T22:38:21.584737200Z" } }, "cell_type": "code", @@ -583,108 +554,24 @@ " return p.part\n", "\n", "\n", - "show(make_bac_recup())" + "# show(make_bac_recup())" ], "id": "a3d65d64eb54186b", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+\n" - ] - } - ], - "execution_count": 164 - }, - { - "metadata": { - "ExecuteTime": { - "end_time": "2026-05-20T01:47:33.197920866Z", - "start_time": "2026-05-20T01:47:31.765776913Z" - } - }, - "cell_type": "code", - "source": [ - "pattes_largueur = 5\n", - "pattes_profondeur = 5\n", - "bras_patte_dist = 8\n", - "bloc_hauteur_totale = 10\n", - "bloc_hauteur = 6\n", - "pourtour_hauteur = bloc_hauteur_totale - bloc_hauteur\n", - "raiser_dim_x = 30\n", - "raiser_dim_y = 18\n", - "raiser_dim_z = 15\n", - "cav_bloc_dim_x = 24\n", - "cav_bloc_dim_y = 14\n", - "\n", - "with BuildPart() as bp:\n", - " raiser = Box(raiser_dim_x, raiser_dim_y, raiser_dim_z)\n", - " raiser_faces = raiser.faces()\n", - " bottom_face = faces().filter_by(Plane.top).sort_by(Axis.Z)[0]\n", - " with BuildSketch(Plane(bottom_face).reverse()):\n", - " Rectangle(cav_bloc_dim_x, cav_bloc_dim_y)\n", - " extrude(amount=(bloc_hauteur + 1), mode=Mode.SUBTRACT)\n", - "\n", - " for face in raiser_faces.filter_by(lambda f: abs(f.normal_at().Z) < 0.01):\n", - " plane = Plane(face)\n", - " offset_plane = plane.offset(bras_patte_dist)\n", - " with BuildSketch(offset_plane):\n", - " with Locations((bottom_face.center().Z,)):\n", - " Rectangle(2, 5, align=(Align.MIN, Align.CENTER))\n", - " with BuildSketch(face):\n", - " add(face.rotate(Axis.Y, 90)) # ← utilise directement la face comme sketch\n", - " loft()\n", - "\n", - " patte_plane = Plane(\n", - " origin=offset_plane.origin,\n", - " x_dir=-offset_plane.y_dir,\n", - " z_dir=(0, 0, 1)\n", - " ).offset(bottom_face.center().Z)\n", - " with BuildSketch(patte_plane):\n", - " Rectangle(5, bras_patte_dist - 1, align=(Align.CENTER, Align.MIN))\n", - " with BuildSketch(patte_plane.offset(-pourtour_hauteur)):\n", - " Rectangle(5, 5, align=(Align.CENTER, Align.MIN))\n", - "\n", - " loft()\n", - "\n", - "set_port(3939)\n", - "show(bp.part)\n", - "export_step(bp.part, \"/tmp/raiser.step\")" - ], - "id": "ff26d94e85b6c380", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "c\n" - ] - }, - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "execution_count": 11 + "outputs": [], + "execution_count": 2 }, { "metadata": { "ExecuteTime": { - "end_time": "2026-05-18T23:37:55.903337866Z", - "start_time": "2026-05-18T23:37:52.010830090Z" + "end_time": "2026-05-25T22:39:38.155100030Z", + "start_time": "2026-05-25T22:39:24.034959895Z" } }, "cell_type": "code", "source": [ "full = Compound(children=[make_couvercle(), make_bac_recup(), make_bloc_extrudeur()])\n", - "export_step(full, \"/tmp/tractotine.step\")" + "export_step(full, \"/tmp/tractotine.step\")\n", + "export_gltf(full, \"/tmp/tractotine.gltf\")" ], "id": "810fb3a101c2fccd", "outputs": [ @@ -694,12 +581,12 @@ "True" ] }, - "execution_count": 166, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 166 + "execution_count": 5 }, { "metadata": { @@ -920,6 +807,14 @@ } ], "execution_count": 17 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "", + "id": "9edcf2da938ea094" } ], "metadata": {