harnice 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- harnice/__init__.py +0 -0
- harnice/__main__.py +4 -0
- harnice/cli.py +234 -0
- harnice/fileio.py +295 -0
- harnice/gui/launcher.py +426 -0
- harnice/lists/channel_map.py +182 -0
- harnice/lists/circuits_list.py +302 -0
- harnice/lists/disconnect_map.py +237 -0
- harnice/lists/formboard_graph.py +63 -0
- harnice/lists/instances_list.py +280 -0
- harnice/lists/library_history.py +40 -0
- harnice/lists/manifest.py +93 -0
- harnice/lists/post_harness_instances_list.py +66 -0
- harnice/lists/rev_history.py +325 -0
- harnice/lists/signals_list.py +135 -0
- harnice/products/__init__.py +1 -0
- harnice/products/cable.py +152 -0
- harnice/products/chtype.py +80 -0
- harnice/products/device.py +844 -0
- harnice/products/disconnect.py +225 -0
- harnice/products/flagnote.py +139 -0
- harnice/products/harness.py +522 -0
- harnice/products/macro.py +10 -0
- harnice/products/part.py +640 -0
- harnice/products/system.py +125 -0
- harnice/products/tblock.py +270 -0
- harnice/state.py +57 -0
- harnice/utils/appearance.py +51 -0
- harnice/utils/circuit_utils.py +326 -0
- harnice/utils/feature_tree_utils.py +183 -0
- harnice/utils/formboard_utils.py +973 -0
- harnice/utils/library_utils.py +333 -0
- harnice/utils/note_utils.py +417 -0
- harnice/utils/svg_utils.py +819 -0
- harnice/utils/system_utils.py +563 -0
- harnice-0.3.0.dist-info/METADATA +32 -0
- harnice-0.3.0.dist-info/RECORD +41 -0
- harnice-0.3.0.dist-info/WHEEL +5 -0
- harnice-0.3.0.dist-info/entry_points.txt +3 -0
- harnice-0.3.0.dist-info/licenses/LICENSE +19 -0
- harnice-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
from harnice import fileio
|
|
2
|
+
from harnice.lists import instances_list
|
|
3
|
+
from harnice.utils import library_utils
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def end_ports_of_circuit(circuit_id):
|
|
8
|
+
"""
|
|
9
|
+
Returns the instance names at the end ports (port 0 and maximum port) of a circuit.
|
|
10
|
+
|
|
11
|
+
Finds and returns the instance names connected to port 0 and the maximum port number
|
|
12
|
+
in the specified circuit. These represent the endpoints of the circuit.
|
|
13
|
+
|
|
14
|
+
**Args:**
|
|
15
|
+
- `circuit_id` (str): Circuit ID to look up. Must be a valid integer string.
|
|
16
|
+
|
|
17
|
+
**Returns:**
|
|
18
|
+
- `tuple`: A tuple of `(zero_port, max_port)` instance names. Either may be empty
|
|
19
|
+
string if not found.
|
|
20
|
+
|
|
21
|
+
**Raises:**
|
|
22
|
+
- `ValueError`: If `circuit_id` is not a valid integer.
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
int(circuit_id)
|
|
26
|
+
except ValueError:
|
|
27
|
+
raise ValueError(f"Pass an integer circuit_id, not '{circuit_id}'")
|
|
28
|
+
zero_port = ""
|
|
29
|
+
max_port = ""
|
|
30
|
+
for instance in fileio.read_tsv("instances list"):
|
|
31
|
+
if instance.get("circuit_id") == circuit_id:
|
|
32
|
+
if instance.get("circuit_port_number") == 0:
|
|
33
|
+
zero_port = instance.get("instance_name")
|
|
34
|
+
if instance.get("circuit_port_number") == max_port_number_in_circuit(
|
|
35
|
+
circuit_id
|
|
36
|
+
):
|
|
37
|
+
max_port = instance.get("instance_name")
|
|
38
|
+
return zero_port, max_port
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def max_port_number_in_circuit(circuit_id):
|
|
42
|
+
"""
|
|
43
|
+
Finds the maximum circuit port number used in a circuit.
|
|
44
|
+
|
|
45
|
+
Scans all instances in the circuit to find the highest port number assigned.
|
|
46
|
+
Circuit instances (`item_type=="circuit"`) are skipped. Blank port numbers
|
|
47
|
+
cause an error unless the instance is a circuit instance.
|
|
48
|
+
|
|
49
|
+
**Args:**
|
|
50
|
+
- `circuit_id` (str): Circuit ID to search.
|
|
51
|
+
|
|
52
|
+
**Returns:**
|
|
53
|
+
- `int`: The maximum port number found in the circuit (`0` if no ports found).
|
|
54
|
+
|
|
55
|
+
**Raises:**
|
|
56
|
+
- `ValueError`: If any non-circuit instance has a blank `circuit_port_number`.
|
|
57
|
+
"""
|
|
58
|
+
max_port_number = 0
|
|
59
|
+
for instance in fileio.read_tsv("instances list"):
|
|
60
|
+
if instance.get("circuit_id") == circuit_id:
|
|
61
|
+
if instance.get("circuit_port_number") == "":
|
|
62
|
+
if instance.get("item_type") == "circuit":
|
|
63
|
+
continue
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"Circuit port number is blank for {instance.get('instance_name')}"
|
|
66
|
+
)
|
|
67
|
+
max_port_number = max(
|
|
68
|
+
max_port_number, int(instance.get("circuit_port_number"))
|
|
69
|
+
)
|
|
70
|
+
return max_port_number
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def squeeze_instance_between_ports_in_circuit(
|
|
74
|
+
instance_name, circuit_id, new_circuit_port_number
|
|
75
|
+
):
|
|
76
|
+
"""
|
|
77
|
+
Inserts an instance into a circuit by shifting existing port numbers.
|
|
78
|
+
|
|
79
|
+
Assigns the specified instance to a port number in the circuit, incrementing
|
|
80
|
+
the port numbers of all instances that were at or after that port number.
|
|
81
|
+
Circuit instances (`item_type=="circuit"`) are skipped and not renumbered.
|
|
82
|
+
|
|
83
|
+
**Args:**
|
|
84
|
+
- `instance_name` (str): Name of the instance to insert into the circuit.
|
|
85
|
+
- `circuit_id` (str): Circuit ID to insert the instance into.
|
|
86
|
+
- `new_circuit_port_number` (int): Port number to assign to the instance. All
|
|
87
|
+
instances at this port number or higher will have their port numbers
|
|
88
|
+
incremented by 1.
|
|
89
|
+
"""
|
|
90
|
+
instances = fileio.read_tsv("instances list")
|
|
91
|
+
for instance in instances:
|
|
92
|
+
if instance.get("instance_name") == instance_name:
|
|
93
|
+
continue
|
|
94
|
+
if instance.get("circuit_id") == circuit_id:
|
|
95
|
+
if instance.get("item_type") == "circuit":
|
|
96
|
+
continue
|
|
97
|
+
old_port_number = instance.get("circuit_port_number")
|
|
98
|
+
if int(instance.get("circuit_port_number")) < new_circuit_port_number:
|
|
99
|
+
continue
|
|
100
|
+
else:
|
|
101
|
+
instances_list.modify(
|
|
102
|
+
instance.get("instance_name"),
|
|
103
|
+
{"circuit_port_number": int(old_port_number) + 1},
|
|
104
|
+
)
|
|
105
|
+
for instance in instances:
|
|
106
|
+
if instance.get("instance_name") == instance_name:
|
|
107
|
+
instances_list.modify(
|
|
108
|
+
instance_name,
|
|
109
|
+
{
|
|
110
|
+
"circuit_id": circuit_id,
|
|
111
|
+
"circuit_port_number": new_circuit_port_number,
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def instances_of_circuit(circuit_id):
|
|
117
|
+
"""
|
|
118
|
+
Returns all instances in a circuit, sorted by port number.
|
|
119
|
+
|
|
120
|
+
Finds all instances (excluding circuit instances themselves) that belong to
|
|
121
|
+
the specified circuit and returns them sorted numerically by their `circuit_port_number`.
|
|
122
|
+
Instances with missing port numbers are sorted last (treated as `999999`).
|
|
123
|
+
|
|
124
|
+
**Args:**
|
|
125
|
+
- `circuit_id` (str): Circuit ID to search for instances.
|
|
126
|
+
|
|
127
|
+
**Returns:**
|
|
128
|
+
- `list`: List of instance dictionaries, sorted by `circuit_port_number` in ascending order.
|
|
129
|
+
"""
|
|
130
|
+
instances = []
|
|
131
|
+
for instance in fileio.read_tsv("instances list"):
|
|
132
|
+
if instance.get("circuit_id") == circuit_id:
|
|
133
|
+
if instance.get("item_type") == "circuit":
|
|
134
|
+
continue
|
|
135
|
+
instances.append(instance)
|
|
136
|
+
|
|
137
|
+
# sort numerically by circuit_port_number, treating missing as large number
|
|
138
|
+
instances.sort(key=lambda x: int(x.get("circuit_port_number") or 999999))
|
|
139
|
+
|
|
140
|
+
return instances
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def instance_of_circuit_port_number(circuit_id, circuit_port_number):
|
|
144
|
+
"""
|
|
145
|
+
Finds the instance name at a specific port number in a circuit.
|
|
146
|
+
|
|
147
|
+
Searches the instances list for an instance that matches both the `circuit_id`
|
|
148
|
+
and `circuit_port_number`. The comparison is done after stripping whitespace
|
|
149
|
+
and converting to strings.
|
|
150
|
+
|
|
151
|
+
**Args:**
|
|
152
|
+
- `circuit_id` (str): Circuit ID to search.
|
|
153
|
+
- `circuit_port_number` (str or int): Port number to search for.
|
|
154
|
+
|
|
155
|
+
**Returns:**
|
|
156
|
+
- `str`: The `instance_name` of the instance at the specified port number.
|
|
157
|
+
|
|
158
|
+
**Raises:**
|
|
159
|
+
- `ValueError`: If `circuit_id` or `circuit_port_number` is blank, or if no instance
|
|
160
|
+
is found matching both the `circuit_id` and `circuit_port_number`.
|
|
161
|
+
"""
|
|
162
|
+
if circuit_id in ["", None]:
|
|
163
|
+
raise ValueError("Circuit ID is blank")
|
|
164
|
+
if circuit_port_number in ["", None]:
|
|
165
|
+
raise ValueError("Circuit port number is blank")
|
|
166
|
+
|
|
167
|
+
for instance in fileio.read_tsv("instances list"):
|
|
168
|
+
if instance.get("circuit_id").strip() == str(circuit_id).strip():
|
|
169
|
+
if (
|
|
170
|
+
instance.get("circuit_port_number").strip()
|
|
171
|
+
== str(circuit_port_number).strip()
|
|
172
|
+
):
|
|
173
|
+
return instance.get("instance_name")
|
|
174
|
+
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"No instance found for circuit {circuit_id} and port number {circuit_port_number}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def circuit_instance_of_instance(instance_name):
|
|
181
|
+
"""
|
|
182
|
+
Returns the circuit instance dictionary for an instance that belongs to a circuit.
|
|
183
|
+
|
|
184
|
+
Finds the circuit instance (`item_type=="circuit"`) that corresponds to a given
|
|
185
|
+
instance. The circuit instance has the same `circuit_id` as the instance's `circuit_id`.
|
|
186
|
+
|
|
187
|
+
**Args:**
|
|
188
|
+
- `instance_name` (str): Name of the instance to find the circuit instance for.
|
|
189
|
+
|
|
190
|
+
**Returns:**
|
|
191
|
+
- `dict`: The circuit instance dictionary.
|
|
192
|
+
|
|
193
|
+
**Raises:**
|
|
194
|
+
- `ValueError`: If the circuit instance cannot be found for the given instance.
|
|
195
|
+
"""
|
|
196
|
+
circuit_instance_name = ""
|
|
197
|
+
instance_rows = fileio.read_tsv("instances list")
|
|
198
|
+
for instance in instance_rows:
|
|
199
|
+
if instance.get("instance_name") == instance_name:
|
|
200
|
+
circuit_instance_name = instance.get("circuit_id")
|
|
201
|
+
break
|
|
202
|
+
for instance in instance_rows:
|
|
203
|
+
if instance.get("circuit_id") == circuit_instance_name:
|
|
204
|
+
if instance.get("instance_name") == instance_name:
|
|
205
|
+
return instance
|
|
206
|
+
raise ValueError(
|
|
207
|
+
f"Circuit instance {circuit_instance_name} of instance {instance_name} not found"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def assign_cable_conductor(
|
|
212
|
+
cable_instance_name, # unique identifier for the cable in your project
|
|
213
|
+
cable_conductor_id, # (container, identifier) tuple identifying the conductor in the cable being imported
|
|
214
|
+
conductor_instance, # instance name of the conductor in your project
|
|
215
|
+
library_info, # dict containing library info: {lib_repo, mpn, lib_subpath, used_rev}
|
|
216
|
+
net, # which net this cable belongs to
|
|
217
|
+
):
|
|
218
|
+
"""
|
|
219
|
+
Assigns a conductor instance to a specific conductor in a cable.
|
|
220
|
+
|
|
221
|
+
Links a conductor instance in the project to a specific conductor within a cable
|
|
222
|
+
by importing the cable from the library (if not already imported) and updating
|
|
223
|
+
the conductor instance with cable assignment information, including the conductor's
|
|
224
|
+
appearance from the cable definition.
|
|
225
|
+
|
|
226
|
+
The `cable_conductor_id` uses the `(container, identifier)` format from the cable
|
|
227
|
+
conductor list. The cable is imported if it doesn't exist, and the conductor
|
|
228
|
+
instance is updated with parent, group, container, identifier, and appearance info.
|
|
229
|
+
|
|
230
|
+
**Args:**
|
|
231
|
+
- `cable_instance_name` (str): Unique identifier for the cable in the project.
|
|
232
|
+
- `cable_conductor_id` (tuple): Tuple of `(container, identifier)` identifying the
|
|
233
|
+
conductor in the cable being imported.
|
|
234
|
+
- `conductor_instance` (str): Instance name of the conductor in the project.
|
|
235
|
+
- `library_info` (dict): Dictionary containing library information with keys:
|
|
236
|
+
`lib_repo`, `mpn`, `lib_subpath`, and optionally `used_rev`.
|
|
237
|
+
- `net` (str): Net name that this cable belongs to.
|
|
238
|
+
|
|
239
|
+
**Raises:**
|
|
240
|
+
- `ValueError`: If the conductor has already been assigned to another instance,
|
|
241
|
+
or if the `conductor_instance` has already been assigned to another cable.
|
|
242
|
+
"""
|
|
243
|
+
# for cable_conductor_id, see (container, identifier) from the cable conductor list.
|
|
244
|
+
# TODO: ensure cable_conductor_id has the right format.
|
|
245
|
+
|
|
246
|
+
instances = fileio.read_tsv("instances list")
|
|
247
|
+
|
|
248
|
+
# --- Make sure conductor of cable has not been assigned yet
|
|
249
|
+
for instance in instances:
|
|
250
|
+
if instance.get("cable_group") == cable_instance_name:
|
|
251
|
+
if instance.get("cable_container") == cable_conductor_id[0]:
|
|
252
|
+
if instance.get("cable_identifier") == cable_conductor_id[1]:
|
|
253
|
+
raise ValueError(
|
|
254
|
+
f"when assingning '{cable_conductor_id} of '{cable_instance_name}' to '{conductor_instance}', "
|
|
255
|
+
f"conductor '{cable_conductor_id}' of '{cable_instance_name}' has already been assigned to {instance.get('instance_name')}"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# --- Make sure conductor instance has not already been assigned to a cable
|
|
259
|
+
for instance in instances:
|
|
260
|
+
if instance.get("instance_name") == conductor_instance:
|
|
261
|
+
if (
|
|
262
|
+
instance.get("cable_group") not in ["", None]
|
|
263
|
+
or instance.get("cable_container") not in ["", None]
|
|
264
|
+
or instance.get("cable_identifier") not in ["", None]
|
|
265
|
+
):
|
|
266
|
+
raise ValueError(
|
|
267
|
+
f"when assingning '{cable_conductor_id} of '{cable_instance_name}' to {conductor_instance}', "
|
|
268
|
+
f"instance '{conductor_instance}' has alredy been assigned to another cable"
|
|
269
|
+
f"to '{instance.get('cable_identifier')}' of cable '{instance.get('cable_group')}'"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
cable_destination_directory = os.path.join(
|
|
273
|
+
fileio.dirpath(None), "instance_data", "cable", cable_instance_name
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
instances_list.new_instance(
|
|
277
|
+
cable_instance_name,
|
|
278
|
+
{
|
|
279
|
+
"net": net,
|
|
280
|
+
"item_type": "cable",
|
|
281
|
+
"location_type": "segment",
|
|
282
|
+
"cable_group": cable_instance_name,
|
|
283
|
+
},
|
|
284
|
+
ignore_duplicates=True,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# --- Import cable from library ---
|
|
288
|
+
library_utils.pull(
|
|
289
|
+
{
|
|
290
|
+
"lib_repo": library_info.get("lib_repo"),
|
|
291
|
+
"lib_subpath": library_info.get("lib_subpath"),
|
|
292
|
+
"item_type": "cable",
|
|
293
|
+
"mpn": library_info.get("mpn"),
|
|
294
|
+
"instance_name": cable_instance_name,
|
|
295
|
+
},
|
|
296
|
+
destination_directory=cable_destination_directory,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
cable_attributes_path = os.path.join(
|
|
300
|
+
cable_destination_directory, f"{cable_instance_name}-conductor_list.tsv"
|
|
301
|
+
)
|
|
302
|
+
cable_attributes = fileio.read_tsv(cable_attributes_path)
|
|
303
|
+
|
|
304
|
+
# --- assign conductor
|
|
305
|
+
for instance in instances:
|
|
306
|
+
if instance.get("instance_name") == conductor_instance:
|
|
307
|
+
appearance = None
|
|
308
|
+
for row in cable_attributes:
|
|
309
|
+
if (
|
|
310
|
+
row.get("container") == cable_conductor_id[0]
|
|
311
|
+
and row.get("identifier") == cable_conductor_id[1]
|
|
312
|
+
):
|
|
313
|
+
appearance = row.get("appearance")
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
instances_list.modify(
|
|
317
|
+
conductor_instance,
|
|
318
|
+
{
|
|
319
|
+
"parent_instance": cable_instance_name,
|
|
320
|
+
"cable_group": cable_instance_name,
|
|
321
|
+
"cable_container": cable_conductor_id[0],
|
|
322
|
+
"cable_identifier": cable_conductor_id[1],
|
|
323
|
+
"appearance": appearance,
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
break
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import runpy
|
|
3
|
+
import math
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
from harnice import fileio
|
|
7
|
+
from harnice.utils import library_utils
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_macro(
|
|
11
|
+
macro_part_number, lib_subpath, lib_repo, artifact_id, base_directory=None, **kwargs
|
|
12
|
+
):
|
|
13
|
+
"""
|
|
14
|
+
Runs a macro script from the library with the given artifact ID.
|
|
15
|
+
|
|
16
|
+
Imports a macro from the library and executes its Python script. The macro
|
|
17
|
+
is pulled into a directory structure and then executed with the `artifact_id`
|
|
18
|
+
and any additional keyword arguments passed as global variables.
|
|
19
|
+
|
|
20
|
+
**Args:**
|
|
21
|
+
- `macro_part_number` (str): Part number of the macro to run.
|
|
22
|
+
- `lib_subpath` (str): Library subpath where the macro is located.
|
|
23
|
+
- `lib_repo` (str): Library repository URL or `"local"` for local library.
|
|
24
|
+
- `artifact_id` (str): Unique identifier for this macro execution (must be unique).
|
|
25
|
+
- `base_directory` (str, optional): Base directory for the macro output. If `None`,
|
|
26
|
+
defaults to `instance_data/macro/{artifact_id}`.
|
|
27
|
+
- `**kwargs`: Additional keyword arguments to pass as global variables to the macro script.
|
|
28
|
+
|
|
29
|
+
**Raises:**
|
|
30
|
+
- `ValueError`: If `artifact_id` is `None`, `macro_part_number` is `None`, `lib_repo` is `None`,
|
|
31
|
+
or if a macro with the given `artifact_id` already exists in library history.
|
|
32
|
+
"""
|
|
33
|
+
if artifact_id is None:
|
|
34
|
+
raise ValueError("artifact_id is required")
|
|
35
|
+
if macro_part_number is None:
|
|
36
|
+
raise ValueError("macro_part_number is required")
|
|
37
|
+
if lib_repo is None:
|
|
38
|
+
raise ValueError("lib_repo is required")
|
|
39
|
+
|
|
40
|
+
for instance in fileio.read_tsv("library history"):
|
|
41
|
+
if instance.get("instance_name") == artifact_id:
|
|
42
|
+
raise ValueError(f"Macro with ID {artifact_id} already exists")
|
|
43
|
+
|
|
44
|
+
if base_directory is None:
|
|
45
|
+
base_directory = os.path.join("instance_data", "macro", artifact_id)
|
|
46
|
+
|
|
47
|
+
os.makedirs(fileio.dirpath(None, base_directory), exist_ok=True)
|
|
48
|
+
|
|
49
|
+
library_utils.pull(
|
|
50
|
+
{
|
|
51
|
+
"mpn": macro_part_number,
|
|
52
|
+
"lib_repo": lib_repo,
|
|
53
|
+
"lib_subpath": lib_subpath,
|
|
54
|
+
"item_type": "macro",
|
|
55
|
+
"instance_name": artifact_id,
|
|
56
|
+
},
|
|
57
|
+
destination_directory=fileio.dirpath(None, base_directory=base_directory),
|
|
58
|
+
update_instances_list=False,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
script_path = os.path.join(
|
|
62
|
+
fileio.dirpath(None, base_directory=base_directory), f"{macro_part_number}.py"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# always pass the basics, but let kwargs override/extend
|
|
66
|
+
init_globals = {
|
|
67
|
+
"artifact_id": artifact_id,
|
|
68
|
+
"artifact_path": base_directory,
|
|
69
|
+
"base_directory": base_directory,
|
|
70
|
+
**kwargs, # merges/overrides
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
runpy.run_path(script_path, run_name="__main__", init_globals=init_globals)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def lookup_outputcsys_from_lib_used(instance, outputcsys, base_directory=None):
|
|
77
|
+
"""
|
|
78
|
+
Looks up coordinate system transform from an instance's library attributes.
|
|
79
|
+
|
|
80
|
+
Reads the instance's attributes JSON file to find the specified output coordinate
|
|
81
|
+
system definition and returns its transform values. If the coordinate system is
|
|
82
|
+
`"origin"`, returns zero transform.
|
|
83
|
+
|
|
84
|
+
**Args:**
|
|
85
|
+
- `instance` (dict): Instance dictionary containing `item_type` and `instance_name`.
|
|
86
|
+
- `outputcsys` (str): Name of the output coordinate system to look up (`"origin"` returns zero transform).
|
|
87
|
+
- `base_directory` (str, optional): Base directory path. If `None`, uses current working directory.
|
|
88
|
+
|
|
89
|
+
**Returns:**
|
|
90
|
+
- `tuple`: A tuple of `(x, y, rotation)` representing the coordinate system transform.
|
|
91
|
+
Returns `(0, 0, 0)` if the coordinate system is `"origin"` or if the attributes
|
|
92
|
+
file is not found.
|
|
93
|
+
|
|
94
|
+
**Raises:**
|
|
95
|
+
- `ValueError`: If the specified output coordinate system is not defined in the
|
|
96
|
+
instance's attributes file.
|
|
97
|
+
"""
|
|
98
|
+
if outputcsys == "origin":
|
|
99
|
+
return 0, 0, 0
|
|
100
|
+
|
|
101
|
+
attributes_path = os.path.join(
|
|
102
|
+
fileio.dirpath(None, base_directory=base_directory),
|
|
103
|
+
"instance_data",
|
|
104
|
+
instance.get("item_type"),
|
|
105
|
+
instance.get("instance_name"),
|
|
106
|
+
f"{instance.get("instance_name")}-attributes.json",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
with open(attributes_path, "r", encoding="utf-8") as f:
|
|
111
|
+
attributes_data = json.load(f)
|
|
112
|
+
except FileNotFoundError:
|
|
113
|
+
return 0, 0, 0
|
|
114
|
+
|
|
115
|
+
csys_children = attributes_data.get("csys_children", {})
|
|
116
|
+
|
|
117
|
+
if outputcsys not in csys_children:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"[ERROR] Output coordinate system '{outputcsys}' not defined in {attributes_path}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
child_csys = csys_children[outputcsys]
|
|
123
|
+
|
|
124
|
+
# Extract values with safe numeric defaults
|
|
125
|
+
x = child_csys.get("x", 0)
|
|
126
|
+
y = child_csys.get("y", 0)
|
|
127
|
+
angle = child_csys.get("angle", 0)
|
|
128
|
+
distance = child_csys.get("distance", 0)
|
|
129
|
+
rotation = child_csys.get("rotation", 0)
|
|
130
|
+
|
|
131
|
+
# Convert angle to radians if it's stored in degrees
|
|
132
|
+
angle_rad = math.radians(angle)
|
|
133
|
+
|
|
134
|
+
# Apply translation based on distance + angle
|
|
135
|
+
x = x + distance * math.cos(angle_rad)
|
|
136
|
+
y = y + distance * math.sin(angle_rad)
|
|
137
|
+
|
|
138
|
+
return x, y, rotation
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def copy_pdfs_to_cwd():
|
|
142
|
+
"""
|
|
143
|
+
Copies all PDF files from `instance_data` directory to the current working directory.
|
|
144
|
+
|
|
145
|
+
Recursively searches the `instance_data` directory tree and copies all PDF files
|
|
146
|
+
found to the current working directory. Preserves file metadata during copy.
|
|
147
|
+
Prints error messages if any files cannot be copied but continues processing.
|
|
148
|
+
"""
|
|
149
|
+
cwd = os.getcwd()
|
|
150
|
+
|
|
151
|
+
for root, _, files in os.walk(fileio.dirpath(None, base_directory="instance_data")):
|
|
152
|
+
for filename in files:
|
|
153
|
+
if filename.lower().endswith(".pdf"):
|
|
154
|
+
source_path = os.path.join(root, filename)
|
|
155
|
+
dest_path = os.path.join(cwd, filename)
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
shutil.copy2(source_path, dest_path) # preserves metadata
|
|
159
|
+
except Exception as e:
|
|
160
|
+
print(f"[ERROR] Could not copy {source_path}: {e}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def run_feature_for_relative(project_key, referenced_pn_rev, feature_tree_utils_name):
|
|
164
|
+
"""
|
|
165
|
+
Runs a feature tree script from a referenced part's `features_for_relatives` directory.
|
|
166
|
+
|
|
167
|
+
Executes a Python script located in the `features_for_relatives` directory of a
|
|
168
|
+
referenced part. This is used to run feature scripts that are associated with
|
|
169
|
+
parts referenced by the current project.
|
|
170
|
+
|
|
171
|
+
**Args:**
|
|
172
|
+
- `project_key` (str): Key identifying the project to look up.
|
|
173
|
+
- `referenced_pn_rev` (tuple): Tuple of `(part_number, revision)` for the referenced part.
|
|
174
|
+
- `feature_tree_utils_name` (str): Filename of the feature tree script to execute.
|
|
175
|
+
"""
|
|
176
|
+
project_path = fileio.get_path_to_project(project_key)
|
|
177
|
+
feature_tree_utils_path = os.path.join(
|
|
178
|
+
project_path,
|
|
179
|
+
f"{referenced_pn_rev[0]}-{referenced_pn_rev[1]}",
|
|
180
|
+
"features_for_relatives",
|
|
181
|
+
feature_tree_utils_name,
|
|
182
|
+
)
|
|
183
|
+
runpy.run_path(feature_tree_utils_path, run_name="__main__")
|