File size: 16,568 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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# -*- coding: utf-8 -*-
# ***************************************************************************
# *   Copyright (c) 2025 FreeCAD Contributors                               *
# *                                                                         *
# *   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
import Path
from CAMTests.PathTestUtils import PathTestBase


class TestPathCommandAnnotations(PathTestBase):
    """Test Path.Command annotations functionality."""

    def test00(self):
        """Test basic annotations property access."""
        # Create empty command
        c = Path.Command()
        self.assertIsInstance(c, Path.Command)

        # Test empty annotations
        self.assertEqual(c.Annotations, {})

        # Set annotations via property
        c.Annotations = {"tool": "tap", "material": "steel"}
        self.assertEqual(c.Annotations, {"tool": "tap", "material": "steel"})

        # Test individual annotation access
        self.assertEqual(c.Annotations.get("tool"), "tap")
        self.assertEqual(c.Annotations.get("material"), "steel")
        self.assertIsNone(c.Annotations.get("nonexistent"))

    def test01(self):
        """Test annotations with command creation."""
        # Create command with parameters
        c = Path.Command("G84", {"X": 10, "Y": 20, "Z": -5})

        # Set annotations
        c.Annotations = {"operation": "tapping", "thread": "M6"}

        # Verify command still works normally
        self.assertEqual(c.Name, "G84")
        self.assertEqual(c.Parameters["X"], 10.0)
        self.assertEqual(c.Parameters["Y"], 20.0)
        self.assertEqual(c.Parameters["Z"], -5.0)

        # Verify annotations are preserved
        self.assertEqual(c.Annotations["operation"], "tapping")
        self.assertEqual(c.Annotations["thread"], "M6")

    def test02(self):
        """Test addAnnotations method with dictionary input."""
        c = Path.Command("G1", {"X": 5, "Y": 5})

        # Test method chaining with dictionary
        result = c.addAnnotations({"note": "test note", "tool": "end mill"})

        # Verify method returns the command object for chaining
        self.assertIs(result, c)

        # Verify annotations were set
        self.assertEqual(c.Annotations["note"], "test note")
        self.assertEqual(c.Annotations["tool"], "end mill")

    def test03(self):
        """Test addAnnotations method with string input."""
        c = Path.Command("G2", {"X": 15, "Y": 15})

        # Test method chaining with string
        result = c.addAnnotations("xyz:abc test:1234 operation:milling")

        # Verify method returns the command object for chaining
        self.assertIs(result, c)

        # Verify annotations were parsed and set correctly
        self.assertEqual(c.Annotations["xyz"], "abc")
        self.assertEqual(c.Annotations["test"], 1234)
        self.assertEqual(c.Annotations["operation"], "milling")

    def test04(self):
        """Test annotations update behavior."""
        c = Path.Command("G0", {"Z": 20})

        # Set initial annotations
        c.Annotations = {"initial": "value"}
        self.assertEqual(c.Annotations, {"initial": "value"})

        # Add more annotations - should merge/update
        c.addAnnotations({"additional": "value2", "initial": "updated"})

        expected = {"initial": "updated", "additional": "value2"}
        self.assertEqual(c.Annotations, expected)

    def test05(self):
        """Test method chaining in fluent interface."""
        # Test the fluent interface - create command and set annotations in one line
        c = Path.Command("G84", {"X": 10, "Y": 10, "Z": 0.0}).addAnnotations("thread:M8 depth:15mm")

        # Verify command parameters
        self.assertEqual(c.Name, "G84")
        self.assertEqual(c.Parameters["X"], 10.0)
        self.assertEqual(c.Parameters["Y"], 10.0)
        self.assertEqual(c.Parameters["Z"], 0.0)

        # Verify annotations
        self.assertEqual(c.Annotations["thread"], "M8")
        self.assertEqual(c.Annotations["depth"], "15mm")

    def test06(self):
        """Test annotations with special characters and edge cases."""
        c = Path.Command("G1")

        # Test annotations with special characters
        c.Annotations = {
            "unicode": "café",
            "numbers": "123.45",
            "empty": "",
            "spaces": "value with spaces",
        }

        self.assertEqual(c.Annotations["unicode"], "café")
        self.assertEqual(c.Annotations["numbers"], "123.45")
        self.assertEqual(c.Annotations["empty"], "")
        self.assertEqual(c.Annotations["spaces"], "value with spaces")

    def test07(self):
        """Test annotations persistence through operations."""
        c = Path.Command("G1", {"X": 10, "Y": 20})
        c.Annotations = {"persistent": "value"}

        # Test that annotations survive parameter changes
        c.Parameters = {"X": 30, "Y": 40}
        self.assertEqual(c.Annotations["persistent"], "value")

        # Test that annotations survive name changes
        c.Name = "G2"
        self.assertEqual(c.Annotations["persistent"], "value")

    def test08(self):
        """Test multiple annotation update methods."""
        c = Path.Command()

        # Method 1: Property assignment
        c.Annotations = {"method1": "property"}

        # Method 2: addAnnotations with dict
        c.addAnnotations({"method2": "dict"})

        # Method 3: addAnnotations with string
        c.addAnnotations("method3:string")

        # Verify all methods worked and annotations are merged
        expected = {"method1": "property", "method2": "dict", "method3": "string"}
        self.assertEqual(c.Annotations, expected)

    def test09(self):
        """Test string parsing edge cases."""
        c = Path.Command()

        # Test various string formats
        c.addAnnotations("simple:value")
        self.assertEqual(c.Annotations["simple"], "value")

        # Test multiple key:value pairs
        c.Annotations = {}  # Clear first
        c.addAnnotations("key1:val1 key2:val2 key3:val3")
        expected = {"key1": "val1", "key2": "val2", "key3": "val3"}
        self.assertEqual(c.Annotations, expected)

        # Test that malformed strings are ignored
        c.Annotations = {}  # Clear first
        c.addAnnotations("valid:value invalid_no_colon")
        self.assertEqual(c.Annotations, {"valid": "value"})

    def test10(self):
        """Test annotations in gcode context."""
        # Create a tapping command with annotations
        c = Path.Command(
            "G84", {"X": 25.0, "Y": 30.0, "Z": -10.0, "R": 2.0, "P": 0.5, "F": 100.0}
        ).addAnnotations("operation:tapping thread:M6x1.0 depth:10mm")

        # Verify gcode output is unaffected by annotations
        gcode = c.toGCode()
        self.assertIn("G84", gcode)
        self.assertIn("X25", gcode)
        self.assertIn("Y30", gcode)
        self.assertIn("Z-10", gcode)

        # Verify annotations are preserved
        self.assertEqual(c.Annotations["operation"], "tapping")
        self.assertEqual(c.Annotations["thread"], "M6x1.0")
        self.assertEqual(c.Annotations["depth"], "10mm")

        # Annotations should not appear in gcode output
        gcode_parts = gcode.split(";", 1)
        main_gcode = gcode_parts[0]
        comment = gcode_parts[1] if len(gcode_parts) > 1 else ""

        self.assertIn("operation:'tapping'", comment)
        self.assertIn("thread:'M6x1.0'", comment)
        self.assertIn("depth:'10mm'", comment)
        self.assertNotIn("operation", main_gcode)
        self.assertNotIn("thread", main_gcode)
        self.assertNotIn("depth", main_gcode)

    def test11(self):
        """Test save/restore with mixed string and numeric annotations (in-memory)."""
        # Create command with mixed annotations
        original = Path.Command("G1", {"X": 10.0, "Y": 20.0, "F": 1000.0})
        original.Annotations = {
            "tool_name": "6mm_endmill",  # string
            "spindle_speed": 12000.0,  # float
            "feed_rate": 1500,  # int -> float
            "operation": "pocket",  # string
            "depth_of_cut": -2.5,  # negative float
        }

        # Use FreeCAD's in-memory serialization
        content = original.dumpContent()

        # Create new command and restore from memory
        restored = Path.Command()
        restored.restoreContent(content)

        # Verify all annotations were restored with correct types
        self.assertEqual(restored.Annotations["tool_name"], "6mm_endmill")
        self.assertEqual(restored.Annotations["spindle_speed"], 12000.0)
        self.assertEqual(restored.Annotations["feed_rate"], 1500.0)
        self.assertEqual(restored.Annotations["operation"], "pocket")
        self.assertEqual(restored.Annotations["depth_of_cut"], -2.5)

        # Verify types are preserved
        self.assertIsInstance(restored.Annotations["tool_name"], str)
        self.assertIsInstance(restored.Annotations["spindle_speed"], float)
        self.assertIsInstance(restored.Annotations["feed_rate"], float)
        self.assertIsInstance(restored.Annotations["operation"], str)
        self.assertIsInstance(restored.Annotations["depth_of_cut"], float)

        # Verify GCode parameters were also restored correctly
        self.assertEqual(restored.Name, "G1")
        # Note: Parameters are restored via GCode parsing

    def test12(self):
        """Test save/restore with empty annotations (in-memory)."""
        # Test empty annotations (should work and use compact format)
        simple = Path.Command("G0", {"Z": 5.0})
        self.assertEqual(simple.Annotations, {})

        simple_content = simple.dumpContent()
        simple_restored = Path.Command()
        simple_restored.restoreContent(simple_content)

        self.assertEqual(simple_restored.Annotations, {})
        self.assertEqual(simple_restored.Name, "G0")

    def test13(self):
        """Test save/restore with complex annotations and edge cases (in-memory)."""
        # Test complex CAM annotations with edge cases
        complex_cmd = Path.Command("G84", {"X": 25.4, "Y": 12.7, "Z": -8.0})
        complex_cmd.Annotations = {
            # Mixed types with edge cases
            "tool_type": "tap",  # string
            "spindle_speed": 500.0,  # float
            "zero_value": 0.0,  # zero
            "negative": -123.456,  # negative
            "large_number": 999999.999,  # large number
            "operation_id": "OP_030",  # alphanumeric string
            "thread_spec": "M4x0.7",  # string with numbers
            "scientific": 1.23e-6,  # scientific notation
        }

        # Serialize and restore
        complex_content = complex_cmd.dumpContent()
        complex_restored = Path.Command()
        complex_restored.restoreContent(complex_content)

        # Verify all complex data restored correctly
        self.assertEqual(len(complex_restored.Annotations), 8)

        # Check specific values and types
        self.assertEqual(complex_restored.Annotations["tool_type"], "tap")
        self.assertIsInstance(complex_restored.Annotations["tool_type"], str)

        self.assertEqual(complex_restored.Annotations["spindle_speed"], 500.0)
        self.assertIsInstance(complex_restored.Annotations["spindle_speed"], float)

        self.assertEqual(complex_restored.Annotations["zero_value"], 0.0)
        self.assertEqual(complex_restored.Annotations["negative"], -123.456)
        self.assertEqual(complex_restored.Annotations["large_number"], 999999.999)

        # Verify strings with numbers stay as strings
        self.assertEqual(complex_restored.Annotations["operation_id"], "OP_030")
        self.assertEqual(complex_restored.Annotations["thread_spec"], "M4x0.7")
        self.assertIsInstance(complex_restored.Annotations["operation_id"], str)
        self.assertIsInstance(complex_restored.Annotations["thread_spec"], str)

        # Check scientific notation (now only 6 decimal places)
        self.assertAlmostEqual(complex_restored.Annotations["scientific"], 1.23e-6, places=6)
        self.assertIsInstance(complex_restored.Annotations["scientific"], float)

    def test14(self):
        """Test Command constructor with positional annotations parameter."""
        # Test constructor with string annotations (positional)
        c1 = Path.Command("G1", {"X": 10.0, "Y": 5.0}, {"note": "Rapid move"})
        self.assertEqual(c1.Name, "G1")
        self.assertEqual(c1.Parameters["X"], 10.0)
        self.assertEqual(c1.Parameters["Y"], 5.0)
        self.assertEqual(c1.Annotations["note"], "Rapid move")

    def test15(self):
        """Test Command constructor with keyword annotations parameter."""
        # Test constructor with annotations keyword argument
        c1 = Path.Command("G1", {"X": 10.0, "Y": 5.0}, annotations={"note": "Rapid move"})
        self.assertEqual(c1.Name, "G1")
        self.assertEqual(c1.Parameters["X"], 10.0)
        self.assertEqual(c1.Parameters["Y"], 5.0)
        self.assertEqual(c1.Annotations["note"], "Rapid move")

    def test16(self):
        """Test Command constructor with numeric annotations."""
        # Test constructor with numeric annotations
        c1 = Path.Command("G2", {"X": 20.0, "Y": 15.0}, {"priority": 1})
        self.assertEqual(c1.Name, "G2")
        self.assertEqual(c1.Parameters["X"], 20.0)
        self.assertEqual(c1.Parameters["Y"], 15.0)
        self.assertEqual(c1.Annotations["priority"], 1)

    def test17(self):
        """Test Command constructor with mixed string and numeric annotations."""
        # Test constructor with mixed annotations
        c1 = Path.Command("G3", {"X": 30.0, "Y": 25.0}, {"note": "Arc move", "speed": 1500})
        self.assertEqual(c1.Name, "G3")
        self.assertEqual(c1.Parameters["X"], 30.0)
        self.assertEqual(c1.Parameters["Y"], 25.0)
        self.assertEqual(c1.Annotations["note"], "Arc move")
        self.assertEqual(c1.Annotations["speed"], 1500)

    def test18(self):
        """Test Command constructor with empty annotations."""
        # Test constructor with empty annotations dict
        c1 = Path.Command("G0", {"X": 0.0, "Y": 0.0}, {})
        self.assertEqual(c1.Name, "G0")
        self.assertEqual(c1.Parameters["X"], 0.0)
        self.assertEqual(c1.Parameters["Y"], 0.0)
        self.assertEqual(c1.Annotations, {})

    def test19(self):
        """Test Command constructor without annotations (backward compatibility)."""
        # Test constructor without annotations (should work as before)
        c1 = Path.Command("G1", {"X": 10.0, "Y": 5.0})
        self.assertEqual(c1.Name, "G1")
        self.assertEqual(c1.Parameters["X"], 10.0)
        self.assertEqual(c1.Parameters["Y"], 5.0)
        self.assertEqual(c1.Annotations, {})

    def test20(self):
        """Test Command constructor with floating point annotations."""
        # Test constructor with floating point annotations
        c1 = Path.Command("G1", {"X": 10.0, "Y": 5.0}, {"retract": 15.5, "angle": 30.25})
        self.assertEqual(c1.Name, "G1")
        self.assertEqual(c1.Parameters["X"], 10.0)
        self.assertEqual(c1.Parameters["Y"], 5.0)
        self.assertEqual(c1.Annotations["retract"], 15.5)
        self.assertEqual(c1.Annotations["angle"], 30.25)