Browse Source

Ajout du rendu isométrique SVG pour build123d

master
hadware 1 month ago
parent
commit
c4282bca1d
  1. 0
      archive/openscad/couvercle.scad
  2. 0
      archive/openscad/inferieure.scad
  3. 0
      archive/openscad/parametres.scad
  4. 0
      archive/openscad/superieure.scad
  5. 0
      archive/tractrudeuse.ipynb
  6. 569
      iso_render.py
  7. 280
      ocp_screenshot.py
  8. 312
      poignee.ipynb
  9. 84
      raiser.ipynb
  10. 332
      test_render.ipynb
  11. 10
      tractocadre.ipynb
  12. 165
      tractrudeuse_v2.ipynb

0
openscad/couvercle.scad → archive/openscad/couvercle.scad

0
openscad/inferieure.scad → archive/openscad/inferieure.scad

0
openscad/parametres.scad → archive/openscad/parametres.scad

0
openscad/superieure.scad → archive/openscad/superieure.scad

0
tractrudeuse.ipynb → archive/tractrudeuse.ipynb

569
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'<svg[^>]+width=["\']([0-9.]+)', svg_str)
h = re.search(r'<svg[^>]+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 <svg> ... </svg>."""
m = re.search(r'<svg[^>]*>(.*)</svg>', 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'<svg xmlns="http://www.w3.org/2000/svg" '
f'xmlns:xlink="http://www.w3.org/1999/xlink" '
f'width="{self.width}" height="{self.height}" '
f'viewBox="0 0 {self.width} {self.height}">',
]
# Defs (filtres shadow si nécessaire)
svg_lines.append('<defs>')
svg_lines.append(
'<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">'
'<feDropShadow dx="3" dy="4" stdDeviation="4" flood-color="#00000033"/>'
'</filter>'
)
# 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'<clipPath id="clip_paper_{i}">'
f'<polygon points="{pts_str}"/>'
f'</clipPath>'
)
svg_lines.append('</defs>')
# Fond
if self.background:
svg_lines.append(
f'<rect width="{self.width}" height="{self.height}" fill="{self.background}"/>'
)
# 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'<polygon points="{pts_str}" '
f'fill="{color}" '
f'stroke="{self.stroke_color}" '
f'stroke-width="{self.stroke_width}" '
f'stroke-linejoin="round"/>'
)
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'<polygon points="{pts_str_paper}" '
f'fill="#00000022" filter="url(#shadow)" />'
)
# Fond de la feuille (avec shading)
paper_color = shade_color(paper.paper_color, normal, ambient=0.7)
svg_lines.append(
f'<polygon points="{pts_str_paper}" '
f'fill="{paper_color}" '
f'stroke="{self.stroke_color}" '
f'stroke-width="{self.stroke_width * 1.2:.2f}" '
f'stroke-linejoin="round"/>'
)
# Contenu SVG de la feuille
self._embed_svg_on_paper(
svg_lines, paper, corners_2d, paper_idx
)
svg_lines.append('</svg>')
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'<g clip-path="url(#clip_paper_{paper_idx})">'
f'<g transform="{transform}">'
+ inner +
f'</g></g>'
)

280
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",
)

312
poignee.ipynb

File diff suppressed because one or more lines are too long

84
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
}

332
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
}

10
tractocadre.ipynb

@ -3,8 +3,8 @@
{ {
"metadata": { "metadata": {
"ExecuteTime": { "ExecuteTime": {
"end_time": "2026-05-22T19:37:45.609594692Z", "end_time": "2026-05-23T01:58:20.511642454Z",
"start_time": "2026-05-22T19:37:43.862365665Z" "start_time": "2026-05-23T01:58:19.038575293Z"
} }
}, },
"cell_type": "code", "cell_type": "code",
@ -126,7 +126,7 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"c++\n" "ccc\n"
] ]
}, },
{ {
@ -135,12 +135,12 @@
"True" "True"
] ]
}, },
"execution_count": 145, "execution_count": 146,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
], ],
"execution_count": 145 "execution_count": 146
}, },
{ {
"metadata": { "metadata": {

165
tractrudeuse_v2.ipynb

@ -6,8 +6,8 @@
"metadata": { "metadata": {
"collapsed": true, "collapsed": true,
"ExecuteTime": { "ExecuteTime": {
"end_time": "2026-05-20T01:46:48.635159570Z", "end_time": "2026-05-25T22:39:19.639342988Z",
"start_time": "2026-05-20T01:46:33.769195980Z" "start_time": "2026-05-25T22:39:17.795171555Z"
} }
}, },
"source": [ "source": [
@ -490,51 +490,22 @@
"\n", "\n",
"gap = 50\n", "gap = 50\n",
"\n", "\n",
"show(\n", "# show(\n",
" make_bloc_extrudeur(),\n", "# make_bloc_extrudeur(),\n",
" make_bac_recup().translate((0, 0, -(bac_hauteur / 2 + bloc_extrudeur_ext_z / 2) + emboitement_profondeur)),\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", "# make_couvercle().translate((0, 0, couv_ext_z / 2 + bloc_extrudeur_ext_z / 2 - emboitement_profondeur)),\n",
" names=[\"superieure\", \"inferieure\", \"couvercle\"],\n", "# names=[\"superieure\", \"inferieure\", \"couvercle\"],\n",
" colors=[\"#ff7800\", \"#000000\", \"#000000\"],\n", "# colors=[\"#ff7800\", \"#000000\", \"#000000\"],\n",
")" "# )"
], ],
"outputs": [ "outputs": [],
{ "execution_count": 4
"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
}, },
{ {
"metadata": { "metadata": {
"ExecuteTime": { "ExecuteTime": {
"end_time": "2026-05-18T23:18:32.922490156Z", "end_time": "2026-05-25T22:38:21.604563409Z",
"start_time": "2026-05-18T23:18:32.423433469Z" "start_time": "2026-05-25T22:38:21.584737200Z"
} }
}, },
"cell_type": "code", "cell_type": "code",
@ -583,108 +554,24 @@
" return p.part\n", " return p.part\n",
"\n", "\n",
"\n", "\n",
"show(make_bac_recup())" "# show(make_bac_recup())"
], ],
"id": "a3d65d64eb54186b", "id": "a3d65d64eb54186b",
"outputs": [ "outputs": [],
{ "execution_count": 2
"name": "stdout",
"output_type": "stream",
"text": [
"+\n"
]
}
],
"execution_count": 164
}, },
{ {
"metadata": { "metadata": {
"ExecuteTime": { "ExecuteTime": {
"end_time": "2026-05-20T01:47:33.197920866Z", "end_time": "2026-05-25T22:39:38.155100030Z",
"start_time": "2026-05-20T01:47:31.765776913Z" "start_time": "2026-05-25T22:39:24.034959895Z"
}
},
"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
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-05-18T23:37:55.903337866Z",
"start_time": "2026-05-18T23:37:52.010830090Z"
} }
}, },
"cell_type": "code", "cell_type": "code",
"source": [ "source": [
"full = Compound(children=[make_couvercle(), make_bac_recup(), make_bloc_extrudeur()])\n", "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", "id": "810fb3a101c2fccd",
"outputs": [ "outputs": [
@ -694,12 +581,12 @@
"True" "True"
] ]
}, },
"execution_count": 166, "execution_count": 5,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
], ],
"execution_count": 166 "execution_count": 5
}, },
{ {
"metadata": { "metadata": {
@ -920,6 +807,14 @@
} }
], ],
"execution_count": 17 "execution_count": 17
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": "",
"id": "9edcf2da938ea094"
} }
], ],
"metadata": { "metadata": {

Loading…
Cancel
Save