// ============================================================================ // Companion Cube — Modèle 3D Complet // Version : 0.2.0 // Architecture : Squelette interne + 6 faces modulaires vissées // ============================================================================ // // STRUCTURE : // 1. Squelette (frame) — Structure porteuse interne en forme de cube // 2. Face Standard (x4) — Module face : coque + shroud + emplacements PCB // 3. Face NFC (dessus) — Comme standard + logement PN532 // 4. Face Qi (dessous) — Comme standard + logement bobine Qi // 5. Électronique — PCB central, batterie (visualisation) // // ASSEMBLAGE : // 1. Monter le PCB central + batterie dans le squelette // 2. Câbler les connecteurs JST vers chaque face // 3. Assembler chaque module face (PCB Anneau + PCB Bouchon dans la coque) // 4. Visser chaque module face sur le squelette (4x M3 par face) // // UTILISATION : // - Ouvrir dans OpenSCAD // - Variable `SHOW_*` pour afficher/masquer les pièces // - Variable `EXPLODE` pour la vue éclatée // - Modules individuels exportables en STL pour l'impression // // ============================================================================ include // --- Contrôle de l'affichage ------------------------------------------------ SHOW_FRAME = true; // Squelette interne SHOW_FACE_TOP = true; // Face dessus (NFC) SHOW_FACE_BOTTOM = true; // Face dessous (Qi) SHOW_FACE_FRONT = true; // Face avant SHOW_FACE_BACK = true; // Face arrière SHOW_FACE_LEFT = true; // Face gauche SHOW_FACE_RIGHT = true; // Face droite SHOW_ELECTRONICS = true; // PCB central + batterie (visualisation) SHOW_PCBS = true; // PCBs anneau + bouchon dans les faces SHOW_SECTION = false; // Coupe en section pour voir l'intérieur EXPLODE = 0; // 0 = assemblé, 1 = vue éclatée (0 à 1 progressif) // Distance d'éclatement EXPLODE_DIST = 40 * EXPLODE; // ============================================================================ // MODULE : Squelette Interne (Frame) // ============================================================================ // Le squelette est constitué des 12 arêtes d'un cube, avec des plots de // fixation M3 sur chaque face intérieure (4 par face = 24 total). // Il accueille le PCB central et la batterie en son centre. // ============================================================================ module frame() { half = CUBE_SIZE / 2; edge_w = FRAME_EDGE_WIDTH; edge_d = FRAME_EDGE_DEPTH; inner = CUBE_SIZE - 2 * edge_d; difference() { union() { // --- 12 arêtes du cube --- // Arêtes selon X (4) for (y = [-1, 1], z = [-1, 1]) { translate([0, y * (half - edge_d/2), z * (half - edge_d/2)]) cube([CUBE_SIZE - 2*edge_d, edge_w, edge_d], center=true); } // Arêtes selon Y (4) for (x = [-1, 1], z = [-1, 1]) { translate([x * (half - edge_d/2), 0, z * (half - edge_d/2)]) cube([edge_d, CUBE_SIZE - 2*edge_d, edge_w], center=true); } // Arêtes selon Z (4) for (x = [-1, 1], y = [-1, 1]) { translate([x * (half - edge_d/2), y * (half - edge_d/2), 0]) cube([edge_d, edge_w, CUBE_SIZE - 2*edge_d], center=true); } // --- 8 coins --- for (x = [-1, 1], y = [-1, 1], z = [-1, 1]) { translate([x * (half - edge_d/2), y * (half - edge_d/2), z * (half - edge_d/2)]) cube([edge_d, edge_d, edge_d], center=true); } // --- Plots de fixation (4 par face, 6 faces) --- // Face +Z (dessus) et -Z (dessous) for (z = [-1, 1]) { for (xy = [ [ (half - edge_d - 10), (half - edge_d - 10)], [-(half - edge_d - 10), (half - edge_d - 10)], [ (half - edge_d - 10), -(half - edge_d - 10)], [-(half - edge_d - 10), -(half - edge_d - 10)] ]) { translate([xy[0], xy[1], z * (half - edge_d)]) _mount_post(z); } } // Face +Y (arrière) et -Y (avant) for (y_dir = [-1, 1]) { for (xz = [ [ (half - edge_d - 10), (half - edge_d - 10)], [-(half - edge_d - 10), (half - edge_d - 10)], [ (half - edge_d - 10), -(half - edge_d - 10)], [-(half - edge_d - 10), -(half - edge_d - 10)] ]) { translate([xz[0], y_dir * (half - edge_d), xz[1]]) rotate([y_dir * 90, 0, 0]) _mount_post(1); } } // Face +X (droite) et -X (gauche) for (x_dir = [-1, 1]) { for (yz = [ [ (half - edge_d - 10), (half - edge_d - 10)], [-(half - edge_d - 10), (half - edge_d - 10)], [ (half - edge_d - 10), -(half - edge_d - 10)], [-(half - edge_d - 10), -(half - edge_d - 10)] ]) { translate([x_dir * (half - edge_d), yz[0], yz[1]]) rotate([0, x_dir * -90, 0]) _mount_post(1); } } // --- Support batterie (berceau central) --- _battery_cradle(); // --- Support PCB central --- _central_pcb_mount(); } // --- Perçages des plots M3 --- _all_mount_holes(); // --- Passages de câbles (JST vers chaque face) --- _cable_channels(); } } // Plot de fixation cylindrique (s'élève vers la face) module _mount_post(direction) { post_h = 8; post_d = 7; translate([0, 0, direction > 0 ? 0 : -post_h]) cylinder(d=post_d, h=post_h); } // Berceau pour la batterie LiPo module _battery_cradle() { bw = BATT_WIDTH + 2 * BATT_TOLERANCE + 4; // +4 pour les parois bl = BATT_LENGTH + 2 * BATT_TOLERANCE + 4; bh = BATT_HEIGHT + BATT_TOLERANCE + 2; wall = 2; // Position : centré, décalé vers le bas pour laisser de la place au PCB central au-dessus translate([0, 0, -10]) difference() { cube([bl, bw, bh], center=true); translate([0, 0, wall]) cube([bl - 2*wall, bw - 2*wall, bh], center=true); // Ouverture pour les fils translate([0, bw/2, 0]) cube([20, wall*3, bh - wall], center=true); } } // Supports pour le PCB central (4 entretoises) module _central_pcb_mount() { standoff_h = 5; standoff_d = 6; hole_d = 2.5; // Pour vis M2.5 ou insert pcb_mount_offset = 8; // Au-dessus du centre (batterie en dessous) for (x = [-1, 1], y = [-1, 1]) { translate([ x * (CENTRAL_PCB_LENGTH/2 - 5), y * (CENTRAL_PCB_WIDTH/2 - 5), pcb_mount_offset ]) difference() { cylinder(d=standoff_d, h=standoff_h); cylinder(d=hole_d, h=standoff_h + 1); } } } // Perçage de tous les trous M3 (traversant les plots) module _all_mount_holes() { half = CUBE_SIZE / 2; edge_d = FRAME_EDGE_DEPTH; // Face +Z et -Z for (z = [-1, 1]) { for (xy = [ [ (half - edge_d - 10), (half - edge_d - 10)], [-(half - edge_d - 10), (half - edge_d - 10)], [ (half - edge_d - 10), -(half - edge_d - 10)], [-(half - edge_d - 10), -(half - edge_d - 10)] ]) { translate([xy[0], xy[1], z * half]) cylinder(d=FRAME_MOUNT_DIAM, h=20, center=true); } } // Face +Y et -Y for (y_dir = [-1, 1]) { for (xz = [ [ (half - edge_d - 10), (half - edge_d - 10)], [-(half - edge_d - 10), (half - edge_d - 10)], [ (half - edge_d - 10), -(half - edge_d - 10)], [-(half - edge_d - 10), -(half - edge_d - 10)] ]) { translate([xz[0], y_dir * half, xz[1]]) rotate([90, 0, 0]) cylinder(d=FRAME_MOUNT_DIAM, h=20, center=true); } } // Face +X et -X for (x_dir = [-1, 1]) { for (yz = [ [ (half - edge_d - 10), (half - edge_d - 10)], [-(half - edge_d - 10), (half - edge_d - 10)], [ (half - edge_d - 10), -(half - edge_d - 10)], [-(half - edge_d - 10), -(half - edge_d - 10)] ]) { translate([x_dir * half, yz[0], yz[1]]) rotate([0, 90, 0]) cylinder(d=FRAME_MOUNT_DIAM, h=20, center=true); } } } // Canaux pour le passage des nappes/câbles JST vers chaque face module _cable_channels() { ch_w = 10; // Largeur du canal ch_h = 6; // Hauteur du canal half = CUBE_SIZE / 2; edge_d = FRAME_EDGE_DEPTH; // Un canal par face, radial depuis le centre // +Z / -Z for (z = [-1, 1]) { translate([0, 0, z * (half - edge_d/2)]) cube([ch_w, ch_h, edge_d + 2], center=true); } // +Y / -Y for (y = [-1, 1]) { translate([0, y * (half - edge_d/2), 0]) cube([ch_w, edge_d + 2, ch_h], center=true); } // +X / -X for (x = [-1, 1]) { translate([x * (half - edge_d/2), 0, 0]) cube([edge_d + 2, ch_w, ch_h], center=true); } } // ============================================================================ // MODULE : Face Standard // ============================================================================ // Module face complet comprenant : // - Coque extérieure avec membrane tactile amincie au centre // - Rainure pour joint torique (étanchéité visuelle) // - Ring Shroud (chambre de diffusion LEDs) // - Emplacements PCB Anneau (étage B) et PCB Bouchon (étage A) // - Trous de fixation M3 (alignés avec les plots du squelette) // ============================================================================ module face_standard() { half = CUBE_SIZE / 2; edge_d = FRAME_EDGE_DEPTH; face_size = CUBE_SIZE - 2 * PRINT_TOLERANCE; difference() { union() { // --- Coque extérieure --- // Plaque principale difference() { // Plaque pleine translate([0, 0, FACE_TOTAL_DEPTH / 2]) cube([face_size - 2*edge_d + 6, face_size - 2*edge_d + 6, FACE_TOTAL_DEPTH], center=true); // Évidement intérieur (laisser les parois) translate([0, 0, FACE_TOTAL_DEPTH / 2 + WALL_THICKNESS]) cube([face_size - 2*edge_d + 6 - 2*WALL_THICKNESS, face_size - 2*edge_d + 6 - 2*WALL_THICKNESS, FACE_TOTAL_DEPTH], center=true); } // --- Membrane tactile (centre, amincie) --- cylinder(d=FACE_CENTER_DIAM + 10, h=FACE_MEMBRANE_T); // --- Paroi extérieure pleine (face visible) --- translate([0, 0, 0]) _face_outer_plate(face_size - 2*edge_d + 6); // --- Ring Shroud (chambre de diffusion) --- translate([0, 0, WALL_THICKNESS]) _ring_shroud(); // --- Oreilles de fixation M3 --- _face_mount_ears(); } // --- Zone tactile amincie (creuser pour ne garder que 1mm) --- translate([0, 0, FACE_MEMBRANE_T]) cylinder(d=FACE_CENTER_DIAM, h=FACE_TOTAL_DEPTH); // --- Rainure joint torique --- translate([0, 0, FACE_TOTAL_DEPTH - 1]) _seal_groove(face_size - 2*edge_d + 2); // --- Trous de fixation M3 --- _face_mount_holes(); // --- Ouverture d'air centrale (côté intérieur) --- translate([0, 0, WALL_THICKNESS]) cylinder(d=AIR_HOLE_DIAM, h=FACE_TOTAL_DEPTH); } } // Plaque extérieure de la face (la surface visible) module _face_outer_plate(size) { // Plaque pleine, épaisseur = WALL_THICKNESS, avec le centre aminci difference() { cube([size, size, WALL_THICKNESS], center=true); // Le centre sera aminci séparément } translate([0, 0, -WALL_THICKNESS/2]) linear_extrude(WALL_THICKNESS) square([size, size], center=true); } // Chambre de diffusion annulaire pour les LEDs module _ring_shroud() { ext = LED_RING_DIAM_EXT + 2 * RING_SHROUD_WALL; int_ = LED_RING_DIAM_INT - 2 * RING_SHROUD_WALL; difference() { cylinder(d=ext, h=RING_SHROUD_HEIGHT); translate([0, 0, -0.5]) cylinder(d=int_, h=RING_SHROUD_HEIGHT + 1); } } // Oreilles de fixation (dépassent pour atteindre les plots du squelette) module _face_mount_ears() { half = CUBE_SIZE / 2; edge_d = FRAME_EDGE_DEPTH; ear_size = 10; for (xy = [ [ (half - edge_d - 10), (half - edge_d - 10)], [-(half - edge_d - 10), (half - edge_d - 10)], [ (half - edge_d - 10), -(half - edge_d - 10)], [-(half - edge_d - 10), -(half - edge_d - 10)] ]) { translate([xy[0], xy[1], FACE_TOTAL_DEPTH/2]) cube([ear_size, ear_size, FACE_TOTAL_DEPTH], center=true); } } // Trous de fixation dans les oreilles module _face_mount_holes() { half = CUBE_SIZE / 2; edge_d = FRAME_EDGE_DEPTH; for (xy = [ [ (half - edge_d - 10), (half - edge_d - 10)], [-(half - edge_d - 10), (half - edge_d - 10)], [ (half - edge_d - 10), -(half - edge_d - 10)], [-(half - edge_d - 10), -(half - edge_d - 10)] ]) { translate([xy[0], xy[1], -1]) cylinder(d=FRAME_MOUNT_DIAM, h=FACE_TOTAL_DEPTH + 2); // Lamage tête de vis (côté extérieur) translate([xy[0], xy[1], -1]) cylinder(d=FRAME_MOUNT_HEAD, h=FRAME_MOUNT_DEPTH + 1); } } // Rainure pour joint torique périphérique module _seal_groove(size) { w = FACE_SEAL_GROOVE_W; d = FACE_SEAL_GROOVE_D; offset = size/2 - w - 1; difference() { cube([size - 2, size - 2, d], center=true); cube([size - 2 - 2*w, size - 2 - 2*w, d + 1], center=true); } } // ============================================================================ // MODULE : Face NFC (Dessus) // ============================================================================ // Identique à la face standard mais avec un logement pour le module PN532 // à la place du simple PCB Bouchon. // ============================================================================ module face_nfc() { difference() { union() { face_standard(); // Logement renforcé pour le PN532 (plus grand que le bouchon standard) translate([0, 0, WALL_THICKNESS + RING_SHROUD_HEIGHT]) _nfc_mount(); } // Ouverture pour l'antenne NFC (dans la membrane) // L'antenne doit être au plus près de la surface pour une bonne lecture translate([0, 0, -1]) cylinder(d=NFC_ANTENNA_DIAM, h=FACE_MEMBRANE_T + 2); } } module _nfc_mount() { // Support carré pour le module PN532 wall = 1.5; size = NFC_PCB_SIZE + 2 * wall + 2 * PRINT_TOLERANCE; inner = NFC_PCB_SIZE + 2 * PRINT_TOLERANCE; difference() { cube([size, size, NFC_PCB_HEIGHT + 2], center=true); cube([inner, inner, NFC_PCB_HEIGHT + 3], center=true); } } // ============================================================================ // MODULE : Face Qi (Dessous) // ============================================================================ // Identique à la face standard mais avec un logement pour la bobine Qi // et son PCB récepteur. // ============================================================================ module face_qi() { difference() { union() { face_standard(); // Logement bobine Qi translate([0, 0, WALL_THICKNESS]) _qi_mount(); } // La bobine Qi doit être au plus près de la surface extérieure // Amincissement supplémentaire au centre pour la bobine translate([0, 0, -0.5]) cylinder(d=QI_COIL_DIAM + 4, h=WALL_THICKNESS - 0.8 + 0.5); } } module _qi_mount() { wall = 1.5; // Anneau de maintien pour la bobine difference() { cylinder(d=QI_COIL_DIAM + 2*wall + 2*PRINT_TOLERANCE, h=QI_COIL_HEIGHT + 1); translate([0, 0, -0.5]) cylinder(d=QI_COIL_DIAM + 2*PRINT_TOLERANCE, h=QI_COIL_HEIGHT + 2); } // Support PCB récepteur Qi (décalé) translate([0, QI_COIL_DIAM/2 + 5, 0]) difference() { cube([QI_PCB_LENGTH + 2*wall, QI_PCB_WIDTH + 2*wall, QI_PCB_HEIGHT + 1], center=true); cube([QI_PCB_LENGTH + PRINT_TOLERANCE, QI_PCB_WIDTH + PRINT_TOLERANCE, QI_PCB_HEIGHT + 2], center=true); } } // ============================================================================ // MODULE : Électronique (Visualisation uniquement) // ============================================================================ // Représentation simplifiée des composants pour vérifier l'encombrement. // Ne pas exporter en STL. // ============================================================================ module electronics() { // --- Batterie LiPo --- color(COLOR_BATTERY) translate([0, 0, -10]) cube([BATT_LENGTH, BATT_WIDTH, BATT_HEIGHT], center=true); // --- PCB Central --- color(COLOR_PCB) translate([0, 0, 8 + 5]) // Au-dessus de la batterie, sur les entretoises cube([CENTRAL_PCB_LENGTH, CENTRAL_PCB_WIDTH, CENTRAL_PCB_HEIGHT], center=true); // --- ESP32-S3 module (sur le PCB central) --- color([0.15, 0.15, 0.15]) translate([0, 0, 8 + 5 + CENTRAL_PCB_HEIGHT/2 + 1.5]) cube([18, 18, 3], center=true); // --- TP4056 (sur le PCB central) --- color(COLOR_PCB) translate([20, 0, 8 + 5 + CENTRAL_PCB_HEIGHT/2 + 1]) cube([10, 8, 2], center=true); } // PCB Anneau (visualisation, placé dans une face) module pcb_ring() { color(COLOR_PCB) difference() { cylinder(d=LED_RING_DIAM_EXT, h=LED_RING_HEIGHT); translate([0, 0, -0.5]) cylinder(d=LED_RING_DIAM_INT, h=LED_RING_HEIGHT + 1); } // LEDs WS2812B color(COLOR_LED) for (i = [0:LED_COUNT-1]) { angle = i * 360 / LED_COUNT; r = (LED_RING_DIAM_EXT + LED_RING_DIAM_INT) / 4; translate([r * cos(angle), r * sin(angle), LED_RING_HEIGHT]) cube([5, 5, 1.5], center=true); // WS2812B = 5x5mm } } // PCB Bouchon (visualisation, placé dans une face) module pcb_cap() { color(COLOR_PCB) cylinder(d=CAP_PCB_DIAM, h=CAP_PCB_HEIGHT); // Pad capacitif (cuivre) color([0.8, 0.6, 0.2]) translate([0, 0, CAP_PCB_HEIGHT]) cylinder(d=CAP_PCB_DIAM - 5, h=0.1); } // Module NFC PN532 (visualisation) module nfc_module() { color(COLOR_NFC) { cube([NFC_PCB_SIZE, NFC_PCB_SIZE, NFC_PCB_HEIGHT], center=true); // Antenne translate([0, 0, NFC_PCB_HEIGHT/2]) cylinder(d=NFC_ANTENNA_DIAM, h=0.5); } } // Bobine Qi (visualisation) module qi_coil() { color(COLOR_QI) { cylinder(d=QI_COIL_DIAM, h=QI_COIL_HEIGHT); // PCB récepteur translate([0, QI_COIL_DIAM/2 + 5, 0]) cube([QI_PCB_LENGTH, QI_PCB_WIDTH, QI_PCB_HEIGHT], center=true); } } // ============================================================================ // ASSEMBLAGE PRINCIPAL // ============================================================================ module assembly() { half = CUBE_SIZE / 2; // --- Squelette --- if (SHOW_FRAME) color(COLOR_FRAME) frame(); // --- Électronique interne --- if (SHOW_ELECTRONICS) electronics(); // --- Face Dessus (+Z) — NFC --- if (SHOW_FACE_TOP) translate([0, 0, half - FACE_TOTAL_DEPTH + EXPLODE_DIST]) color(COLOR_FACE) face_nfc(); // --- Face Dessous (-Z) — Qi --- if (SHOW_FACE_BOTTOM) translate([0, 0, -(half - FACE_TOTAL_DEPTH) - EXPLODE_DIST]) rotate([180, 0, 0]) color(COLOR_FACE) face_qi(); // --- Face Avant (-Y) --- if (SHOW_FACE_FRONT) translate([0, -(half - FACE_TOTAL_DEPTH) - EXPLODE_DIST, 0]) rotate([90, 0, 0]) color(COLOR_FACE) face_standard(); // --- Face Arrière (+Y) --- if (SHOW_FACE_BACK) translate([0, (half - FACE_TOTAL_DEPTH) + EXPLODE_DIST, 0]) rotate([-90, 0, 0]) color(COLOR_FACE) face_standard(); // --- Face Gauche (-X) --- if (SHOW_FACE_LEFT) translate([-(half - FACE_TOTAL_DEPTH) - EXPLODE_DIST, 0, 0]) rotate([0, -90, 0]) color(COLOR_FACE) face_standard(); // --- Face Droite (+X) --- if (SHOW_FACE_RIGHT) translate([(half - FACE_TOTAL_DEPTH) + EXPLODE_DIST, 0, 0]) rotate([0, 90, 0]) color(COLOR_FACE) face_standard(); // --- PCBs dans les faces (visualisation) --- if (SHOW_PCBS) { // Anneau + bouchon dans chaque face (exemple face dessus) translate([0, 0, half - FACE_TOTAL_DEPTH + WALL_THICKNESS + RING_SHROUD_HEIGHT + EXPLODE_DIST]) { pcb_ring(); translate([0, 0, LED_RING_HEIGHT + CAP_CONNECTOR_H]) pcb_cap(); } // NFC module (dessus) translate([0, 0, half - 8 + EXPLODE_DIST]) nfc_module(); // Qi coil (dessous) translate([0, 0, -(half - 4) - EXPLODE_DIST]) qi_coil(); } } // ============================================================================ // RENDU // ============================================================================ if (SHOW_SECTION) { difference() { assembly(); // Coupe en section (plan YZ, moitié +X) translate([CUBE_SIZE/2, 0, 0]) cube([CUBE_SIZE, CUBE_SIZE * 2, CUBE_SIZE * 2], center=true); } } else { assembly(); } // ============================================================================ // MODULES D'EXPORT (décommenter pour exporter individuellement en STL) // ============================================================================ // Pour exporter une pièce : décommenter la ligne, commenter assembly(), // puis Exporter en STL (F6 → File → Export STL) // frame(); // → frame.stl // face_standard(); // → face-standard.stl (x4) // face_nfc(); // → face-nfc.stl (x1, dessus) // face_qi(); // → face-qi.stl (x1, dessous)