Ajout du rendu isométrique SVG pour build123d
This commit is contained in:
+569
@@ -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>'
|
||||||
|
)
|
||||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+5
-5
@@ -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": {
|
||||||
|
|||||||
+30
-135
@@ -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": {
|
||||||
|
|||||||
Reference in New Issue
Block a user