feat(configurator): server-side surface area calculation from STL
Add trimesh-based surface area calculation for uploaded STL files: - New /fp/configurator/calculate_surface_area jsonrpc endpoint - action_calculate_surface_area() method on fp.part.catalog - "Calculate from 3D Model" button visible when a 3D model is attached - Returns area in sq in (converted from mm2), vertex/face counts, and bounding box dimensions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,4 +3,5 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import configurator_controller
|
||||
@@ -0,0 +1,52 @@
|
||||
# -*- 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/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)}
|
||||
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class FpPartCatalog(models.Model):
|
||||
@@ -63,3 +63,39 @@ class FpPartCatalog(models.Model):
|
||||
('fp_part_catalog_partner_partnum_uniq', 'unique(partner_id, part_number)',
|
||||
'Part number must be unique per customer.'),
|
||||
]
|
||||
|
||||
def action_calculate_surface_area(self):
|
||||
"""Calculate surface area from the uploaded 3D model file."""
|
||||
self.ensure_one()
|
||||
if not self.model_attachment_id:
|
||||
from odoo.exceptions import UserError
|
||||
raise UserError(_('No 3D model file uploaded.'))
|
||||
|
||||
try:
|
||||
import trimesh
|
||||
except ImportError:
|
||||
from odoo.exceptions import UserError
|
||||
raise UserError(_('trimesh library not installed on the server. Contact your administrator.'))
|
||||
|
||||
import base64
|
||||
import io
|
||||
|
||||
raw = base64.b64decode(self.model_attachment_id.datas)
|
||||
mesh = trimesh.load(io.BytesIO(raw), file_type='stl')
|
||||
area_mm2 = mesh.area
|
||||
area_sqin = area_mm2 / 645.16
|
||||
|
||||
self.surface_area = round(area_sqin, 4)
|
||||
self.surface_area_uom = 'sq_in'
|
||||
self.geometry_source = '3d_model'
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Surface Area Calculated'),
|
||||
'message': _('%.4f sq in (%.2f mm\u00b2) from %d faces') % (area_sqin, area_mm2, len(mesh.faces)),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -44,7 +44,14 @@
|
||||
<field name="geometry_source"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="surface_area"/>
|
||||
<label for="surface_area"/>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<field name="surface_area" class="oe_inline"/>
|
||||
<button name="action_calculate_surface_area" type="object"
|
||||
string="Calculate from 3D Model"
|
||||
class="btn-link" icon="fa-calculator"
|
||||
invisible="not model_attachment_id"/>
|
||||
</div>
|
||||
<field name="surface_area_uom"/>
|
||||
<field name="weight"/>
|
||||
</group>
|
||||
|
||||
Reference in New Issue
Block a user