File size: 6,801 Bytes
985c397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *   Copyright (c) 2025 Samuel Abels <knipknap@gmail.com>                  *
# *                                                                         *
# *   This program is free software; you can redistribute it and/or modify  *
# *   it under the terms of the GNU Lesser General Public License (LGPL)    *
# *   as published by the Free Software Foundation; either version 2 of     *
# *   the License, or (at your option) any later version.                   *
# *   for detail see the LICENCE text file.                                 *
# *                                                                         *
# *   This program is distributed in the hope that it will be useful,       *
# *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
# *   GNU Library General Public License for more details.                  *
# *                                                                         *
# *   You should have received a copy of the GNU Library General Public     *
# *   License along with this program; if not, write to the Free Software   *
# *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
# *   USA                                                                   *
# *                                                                         *
# ***************************************************************************
import FreeCAD


def to_json(value):
    """Convert a value to JSON format."""
    if isinstance(value, FreeCAD.Units.Quantity):
        return value.UserString
    return value


def units_from_json(params):
    """
    Infer Units (Metric/Imperial) from JSON parameter strings.

    For JSON files from disk, values are stored as strings like "3.175 in" or "6 mm".
    This function examines common dimensional parameters (Diameter, Length, CuttingEdgeHeight, etc.)
    to determine if the toolbit uses metric or imperial units.

    Args:
        params: Dictionary of parameters from JSON (before conversion to FreeCAD.Units.Quantity)

    Returns:
        str: "Metric" or "Imperial", or None if units cannot be determined
    """
    if not isinstance(params, dict):
        return None

    imperial_count = 0
    metric_count = 0

    for param_name in ("Diameter", "ShankDiameter", "Length", "CuttingEdgeLength"):
        value = params.get(param_name)
        if value is not None:
            # Check if it's a string with unit suffix
            if isinstance(value, str):
                value_lower = value.lower().strip()

                # Check for imperial units
                if any(unit in value_lower for unit in ["in", "inch", '"', "thou"]):
                    imperial_count += 1
                # Check for metric units
                elif any(unit in value_lower for unit in ["mm", "cm", "m "]):
                    metric_count += 1

        # Make a decision based on counts
        if imperial_count > metric_count:
            return "Imperial"
        elif metric_count > imperial_count:
            return "Metric"

    return "Metric"  # Default to Metric if uncertain


def format_value(
    value: FreeCAD.Units.Quantity | int | float | None,
    precision: int | None = None,
    units: str | None = None,
) -> str | None:
    """
    Format a numeric value as a string, optionally appending a unit and controlling precision.

    This function uses the ToolBitSchema (via setToolBitSchema) to ensure that units are formatted according to the correct schema (Metric or Imperial) when a FreeCAD.Units.Quantity is provided. The schema is temporarily set for formatting and then restored.

    Args:
        value: The numeric value to format.
        unit: (Optional) The unit string to append (e.g., 'mm', 'in').
        precision: (Optional) Number of decimal places (default: 3).

    Returns:
        str: The formatted value as a string, with unit if provided.
    """
    if value is None:
        return None
    elif isinstance(value, FreeCAD.Units.Quantity):
        if precision is not None:
            user_val, _, user_unit = value.getUserPreferred()
            if user_unit in ("deg", "°", "degree", "degrees"):
                # Remove the last character (degree symbol) and convert to float
                try:
                    deg_val = float(str(user_val)[:-1])
                except Exception:
                    return value.getUserPreferred()[0]
                formatted_value = f"{deg_val:.1f}".rstrip("0").rstrip(".")
                return f"{formatted_value}°"
            # Format the value with the specified number of precision and strip trailing zeros
            setToolBitSchema(units)
            _value = value.getUserPreferred()[0]
            setToolBitSchema()
            return _value
        return value.UserString
    return str(value)


def is_imperial_pitch(pitch_mm, tol=1e-6):
    """
    Classify a pitch in mm as imperial vs metric.
    Rule:
        - If pitch_mm is ~2 decimal places clean -> metric,
        unless it corresponds to an exact whole-number TPI.
        - Otherwise, treat as imperial.
    """
    import math

    try:
        mm = float(pitch_mm)
    except Exception:
        return False
    if mm <= 0:
        return False

    # Check if it's "two-decimal clean"
    two_dec_clean = abs(mm - round(mm, 2)) <= tol

    # Compute TPI
    tpi = 25.4 / mm
    whole_tpi = round(tpi)
    is_whole_tpi = math.isclose(tpi, whole_tpi, abs_tol=1e-6)

    if two_dec_clean and not is_whole_tpi:
        return False  # metric
    return True  # imperial


def setToolBitSchema(schema=None):
    """
    Set the FreeCAD units schema. If passed 'Metric' or 'Imperial', set accordingly (case-insensitive).
    Otherwise, if a document is open, set to its schema. If no document, fallback to user preference or provided schema.
    """
    units_schema_map = {
        "metric": 6,  # 6 = Metric schema in FreeCAD
        "imperial": 3,  # 3 = Imperial schema in FreeCAD
    }
    if isinstance(schema, str) and schema.lower() in units_schema_map:
        FreeCAD.Units.setSchema(units_schema_map[schema.lower()])
        return
    if FreeCAD.ActiveDocument is not None:
        try:
            doc_schema = FreeCAD.ActiveDocument.getSchema()
            FreeCAD.Units.setSchema(doc_schema)
            return
        except Exception:
            pass
    # Fallback to user preference or provided schema
    if schema is None:
        schema = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Units").GetInt(
            "UserSchema", 6
        )
    FreeCAD.Units.setSchema(schema)