102 lines
4.0 KiB
Python
102 lines
4.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
# Part of the Fusion Plating product family.
|
|
|
|
import base64
|
|
import io
|
|
import logging
|
|
|
|
from odoo import http
|
|
from odoo.http import request
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class FpConfiguratorController(http.Controller):
|
|
|
|
@http.route('/fp/3d-viewer', type='http', auth='user', website=False)
|
|
def viewer_3d(self, **kw):
|
|
"""Serve the standalone 3D viewer HTML page.
|
|
|
|
Query params: id (attachment ID), name (filename for format detection).
|
|
The HTML page loads Online3DViewer and renders the model.
|
|
"""
|
|
from odoo.modules.module import get_module_path
|
|
import os
|
|
mod_path = get_module_path('fusion_plating_configurator')
|
|
html_path = os.path.join(
|
|
mod_path, 'static', 'src', 'html', '3d_viewer.html',
|
|
)
|
|
with open(html_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
return request.make_response(content, headers=[
|
|
('Content-Type', 'text/html; charset=utf-8'),
|
|
])
|
|
|
|
@http.route('/fp/3d-model/<int:attachment_id>/<string:filename>',
|
|
type='http', auth='user', website=False)
|
|
def serve_3d_model(self, attachment_id, filename, **kw):
|
|
"""Serve a 3D model file from ir.attachment.
|
|
|
|
This bypasses the /web/content auth issues when loading inside
|
|
an iframe. The filename in the URL ensures Online3DViewer can
|
|
detect the format from the extension.
|
|
"""
|
|
attachment = request.env['ir.attachment'].browse(attachment_id)
|
|
if not attachment.exists():
|
|
return request.not_found()
|
|
raw = base64.b64decode(attachment.datas)
|
|
# Map common CAD extensions to MIME types
|
|
mime_map = {
|
|
'.step': 'application/step', '.stp': 'application/step',
|
|
'.iges': 'application/iges', '.igs': 'application/iges',
|
|
'.stl': 'application/sla',
|
|
'.brep': 'application/octet-stream', '.brp': 'application/octet-stream',
|
|
'.obj': 'text/plain', '.gltf': 'model/gltf+json', '.glb': 'model/gltf-binary',
|
|
}
|
|
import os
|
|
ext = os.path.splitext(filename)[1].lower()
|
|
content_type = mime_map.get(ext, 'application/octet-stream')
|
|
return request.make_response(raw, headers=[
|
|
('Content-Type', content_type),
|
|
('Content-Disposition', f'inline; filename="{filename}"'),
|
|
('Content-Length', str(len(raw))),
|
|
])
|
|
|
|
@http.route('/fp/configurator/calculate_surface_area', type='jsonrpc', auth='user')
|
|
def calculate_surface_area(self, attachment_id, **kw):
|
|
"""Calculate surface area from an uploaded STL file using trimesh."""
|
|
attachment = request.env['ir.attachment'].browse(int(attachment_id))
|
|
if not attachment.exists():
|
|
return {'error': 'Attachment not found.'}
|
|
|
|
try:
|
|
import trimesh
|
|
except ImportError:
|
|
return {'error': 'trimesh library not installed. Run: pip install trimesh'}
|
|
|
|
try:
|
|
raw = base64.b64decode(attachment.datas)
|
|
mesh = trimesh.load(io.BytesIO(raw), file_type='stl')
|
|
|
|
# trimesh returns area in the file's native units (usually mm²)
|
|
area_mm2 = mesh.area
|
|
area_sqin = area_mm2 / 645.16 # mm² to sq in
|
|
|
|
return {
|
|
'surface_area': round(area_sqin, 4),
|
|
'surface_area_mm2': round(area_mm2, 2),
|
|
'unit': 'sq_in',
|
|
'vertex_count': len(mesh.vertices),
|
|
'face_count': len(mesh.faces),
|
|
'bounding_box': {
|
|
'x': round(float(mesh.bounding_box.extents[0]), 2),
|
|
'y': round(float(mesh.bounding_box.extents[1]), 2),
|
|
'z': round(float(mesh.bounding_box.extents[2]), 2),
|
|
},
|
|
}
|
|
except Exception as e:
|
|
_logger.warning('STL surface area calculation failed: %s', e)
|
|
return {'error': str(e)}
|