Task 465: .NWF File Format

Task 465: .NWF File Format

File Format Specifications for the .NWF File Format

The .NWF file format refers to the PTC Creo Neutral Wire Format, a text-based format used in PTC Creo for exchanging electrical cabling information. It facilitates the import and export of logical data related to wiring, cables, connectors, and associated parameters between Creo applications and other systems. The format is ASCII text, with statements separated by whitespace, and supports comments starting with '!' or '#'. All names and parameters are case-insensitive but converted to uppercase during processing. The structure consists of commands that define actions and primary types that represent cabling elements.

1. List of All Properties Intrinsic to the .NWF File Format

The properties are organized into commands (actions or definitions), primary types (core elements), and parameters (key-value attributes). Parameters are user-definable but typically include standard ones for cabling properties. The following tables summarize them based on the format's structure:

Commands:

Command Description Syntax Example
NEW Defines a new primary type. NEW WIRE_SPOOL spool_name
PARAMETER Defines a single parameter for the current item. PARAMETER COLOR BLUE
PARAMETERS Defines multiple parameter names for the current item (followed by VALUES). PARAMETERS COLOR WIRE_GAUGE
VALUES Provides values for the preceding PARAMETERS statement. VALUES BLUE 14
ATTACH Defines connections between items (e.g., pins or components). ATTACH J1 1 J2 1
CONDUCTOR Defines a conductor within a cable (requires prior CABLE or CABLE_SPOOL). CONDUCTOR 1 BL
PIN Defines a pin within a connector or component (requires prior CONNECTOR or COMPONENT). PIN 1

Primary Types:

Primary Type Description Associated Elements
WIRE_SPOOL Defines a wire spool with parameters. PARAMETERS (e.g., COLOR, MIN_BEND_RADIUS, THICKNESS, WIRE_GAUGE, UNITS, MASS_UNITS, WIRE_CONSTRUCTION).
CABLE_SPOOL Defines a cable spool with conductors. PARAMETERS, CONDUCTOR statements with their own PARAMETERS.
CONNECTOR Defines a connector with pins. PARAMETERS (e.g., MODEL_NAME, NUM_OF_PINS, GENDER, DESCRIPTION, LAYER), PIN statements with PARAMETERS (e.g., SIGNAL_NAME, SIGNAL_VALUE, ENTRY_PORT, GROUPING, INTERNAL_LEN, LOGICAL_NAME, DEF_GROUPING).
COMPONENT Defines a component with pins. Similar to CONNECTOR: PARAMETERS, PIN statements.
RAIL Defines a rail (no pins; use "" in ATTACH). PARAMETERS (limited; primarily name).
WIRE Defines a wire referencing a spool. PARAMETERS, ATTACH.
CABLE Defines a cable referencing a spool, with conductors. PARAMETERS, CONDUCTOR with ATTACH.

Common Parameters (User-Definable but Standard in Usage):

  • COLOR: Specifies color (e.g., BLUE).
  • MIN_BEND_RADIUS: Minimum bend radius (numeric, with UNITS).
  • THICKNESS: Thickness (numeric, with UNITS).
  • WIRE_GAUGE: Wire gauge size (numeric).
  • UNITS: Measurement units (e.g., INCH, MM).
  • MASS_UNITS: Mass units (e.g., POUND).
  • WIRE_CONSTRUCTION: Construction type (e.g., STRANDED).
  • MODEL_NAME: Model name for connectors/components.
  • NUM_OF_PINS: Number of pins.
  • GENDER: Connector gender (e.g., FEMALE).
  • SIGNAL_NAME: Signal identifier.
  • SIGNAL_VALUE: Signal value (e.g., +5V).
  • ENTRY_PORT: Entry port name (e.g., CSYS_1).
  • GROUPING: Pin grouping (e.g., ROUND, FLAT).
  • INTERNAL_LEN: Internal length (numeric).
  • LOGICAL_NAME: Logical pin name.
  • DEF_GROUPING: Default grouping.
  • LAYER: Layer assignment (numeric).
  • NAME_FORMAT: Naming convention (e.g., W-##).
  • TYPE: Ignored during import/export (reserved).
  • OBJ_TYPE: Ignored during import/export (reserved).

General rules include: spools must precede referencing wires/cables; statements limited to 1023 characters; comments ignored.

These links point to zipped archives containing .NWF files, as direct uncompressed .NWF downloads are not commonly hosted publicly.

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .NWF File Dump

The following is embeddable HTML with JavaScript code suitable for a Ghost blog post. It creates a drop zone where users can drag and drop a .NWF file. The script reads the file as text, parses it line by line to extract commands, primary types, and parameters, and displays them in a structured output on the screen.

Drag and drop a .NWF file here


4. Python Class for Handling .NWF Files

The following Python class can open, parse (decode/read), write, and print properties from a .NWF file.

import json

class NWFHandler:
    def __init__(self):
        self.data = {
            'wire_spools': [], 'cable_spools': [], 'connectors': [], 'components': [],
            'wires': [], 'cables': [], 'rails': []
        }

    def read(self, filepath):
        with open(filepath, 'r') as f:
            content = f.read()
        lines = [line for line in content.split('\n') if line.strip() and not line.strip().startswith(('!', '#'))]
        current_item = None
        current_type = None
        current_sub = None

        for line in lines:
            parts = line.strip().split()
            command = parts[0]
            if command == 'NEW':
                current_type = parts[1]
                name = parts[2] if len(parts) > 2 else ''
                current_item = {'name': name, 'parameters': {}, 'conductors': [], 'pins': [], 'attachments': []}
                if current_type == 'CABLE_SPOOL':
                    current_item['num_conductors'] = parts[3] if len(parts) > 3 else 0
                elif current_type in ('WIRE', 'CABLE'):
                    current_item['spool'] = parts[3] if len(parts) > 3 else ''
                self.data[current_type.lower() + 's'].append(current_item)
                current_sub = None
            elif command == 'PARAMETER' and current_item:
                key = parts[1]
                value = ' '.join(parts[2:])
                (current_sub or current_item)['parameters'][key] = value
            elif command == 'PARAMETERS' and current_item:
                (current_sub or current_item)['param_names'] = parts[1:]
            elif command == 'VALUES' and current_item and 'param_names' in (current_sub or current_item):
                names = (current_sub or current_item).pop('param_names')
                for i, val in enumerate(parts[1:]):
                    if i < len(names):
                        (current_sub or current_item)['parameters'][names[i]] = val
            elif command == 'CONDUCTOR' and current_item and current_type in ('CABLE', 'CABLE_SPOOL'):
                cond = {'id': parts[1], 'name': parts[2] if len(parts) > 2 else '', 'parameters': {}, 'attachments': []}
                current_item['conductors'].append(cond)
                current_sub = cond
            elif command == 'PIN' and current_item and current_type in ('CONNECTOR', 'COMPONENT'):
                pin = {'name': parts[1], 'parameters': {}}
                current_item['pins'].append(pin)
                current_sub = pin
            elif command == 'ATTACH' and current_item:
                attach = {'from_comp': parts[1], 'from_pin': parts[2], 'to_comp': parts[3], 'to_pin': parts[4] if len(parts) > 4 else ''}
                (current_sub or current_item)['attachments'].append(attach)

    def print_properties(self):
        print(json.dumps(self.data, indent=4))

    def write(self, filepath):
        with open(filepath, 'w') as f:
            for category, items in self.data.items():
                for item in items:
                    if category == 'wire_spools':
                        f.write(f"NEW WIRE_SPOOL {item['name']}\n")
                    elif category == 'cable_spools':
                        f.write(f"NEW CABLE_SPOOL {item['name']} {item.get('num_conductors', 0)}\n")
                    elif category == 'connectors':
                        f.write(f"NEW CONNECTOR {item['name']}\n")
                    elif category == 'components':
                        f.write(f"NEW COMPONENT {item['name']}\n")
                    elif category == 'wires':
                        f.write(f"NEW WIRE {item['name']} {item.get('spool', '')}\n")
                    elif category == 'cables':
                        f.write(f"NEW CABLE {item['name']} {item.get('spool', '')}\n")
                    elif category == 'rails':
                        f.write(f"NEW RAIL {item['name']}\n")
                    for key, val in item['parameters'].items():
                        f.write(f"PARAMETER {key} {val}\n")
                    for cond in item['conductors']:
                        f.write(f"CONDUCTOR {cond['id']} {cond.get('name', '')}\n")
                        for key, val in cond['parameters'].items():
                            f.write(f"PARAMETER {key} {val}\n")
                        for att in cond['attachments']:
                            f.write(f"ATTACH {att['from_comp']} {att['from_pin']} {att['to_comp']} {att['to_pin']}\n")
                    for pin in item['pins']:
                        f.write(f"PIN {pin['name']}\n")
                        for key, val in pin['parameters'].items():
                            f.write(f"PARAMETER {key} {val}\n")
                    for att in item['attachments']:
                        f.write(f"ATTACH {att['from_comp']} {att['from_pin']} {att['to_comp']} {att['to_pin']}\n")
                    f.write('\n')

# Example usage:
# handler = NWFHandler()
# handler.read('example.nwf')
# handler.print_properties()
# handler.write('output.nwf')

5. Java Class for Handling .NWF Files

The following Java class can open, parse, write, and print properties from a .NWF file.

import java.io.*;
import java.util.*;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class NWFHandler {
    private Map<String, List<Map<String, Object>>> data = new HashMap<>();

    public NWFHandler() {
        data.put("wire_spools", new ArrayList<>());
        data.put("cable_spools", new ArrayList<>());
        data.put("connectors", new ArrayList<>());
        data.put("components", new ArrayList<>());
        data.put("wires", new ArrayList<>());
        data.put("cables", new ArrayList<>());
        data.put("rails", new ArrayList<>());
    }

    public void read(String filepath) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(filepath))) {
            String line;
            Map<String, Object> currentItem = null;
            String currentType = null;
            Map<String, Object> currentSub = null;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (line.isEmpty() || line.startsWith("!") || line.startsWith("#")) continue;
                String[] parts = line.split("\\s+");
                String command = parts[0];
                if (command.equals("NEW")) {
                    currentType = parts[1];
                    String name = parts.length > 2 ? parts[2] : "";
                    currentItem = new HashMap<>();
                    currentItem.put("name", name);
                    currentItem.put("parameters", new HashMap<String, String>());
                    currentItem.put("conductors", new ArrayList<Map<String, Object>>());
                    currentItem.put("pins", new ArrayList<Map<String, Object>>());
                    currentItem.put("attachments", new ArrayList<Map<String, String>>());
                    if (currentType.equals("CABLE_SPOOL")) {
                        currentItem.put("num_conductors", parts.length > 3 ? parts[3] : "0");
                    } else if (currentType.equals("WIRE") || currentType.equals("CABLE")) {
                        currentItem.put("spool", parts.length > 3 ? parts[3] : "");
                    }
                    data.get(currentType.toLowerCase() + "s").add(currentItem);
                    currentSub = null;
                } else if (command.equals("PARAMETER") && currentItem != null) {
                    String key = parts[1];
                    String value = String.join(" ", Arrays.copyOfRange(parts, 2, parts.length));
                    ((Map<String, String>) (currentSub != null ? currentSub.get("parameters") : currentItem.get("parameters"))).put(key, value);
                } else if (command.equals("PARAMETERS") && currentItem != null) {
                    List<String> names = Arrays.asList(Arrays.copyOfRange(parts, 1, parts.length));
                    (currentSub != null ? currentSub : currentItem).put("param_names", names);
                } else if (command.equals("VALUES") && currentItem != null && (currentSub != null ? currentSub : currentItem).containsKey("param_names")) {
                    List<String> names = (List<String>) (currentSub != null ? currentSub : currentItem).remove("param_names");
                    for (int i = 1; i < parts.length; i++) {
                        if (i - 1 < names.size()) {
                            ((Map<String, String>) (currentSub != null ? currentSub.get("parameters") : currentItem.get("parameters"))).put(names.get(i - 1), parts[i]);
                        }
                    }
                } else if (command.equals("CONDUCTOR") && currentItem != null && (currentType.equals("CABLE") || currentType.equals("CABLE_SPOOL"))) {
                    Map<String, Object> cond = new HashMap<>();
                    cond.put("id", parts[1]);
                    cond.put("name", parts.length > 2 ? parts[2] : "");
                    cond.put("parameters", new HashMap<String, String>());
                    cond.put("attachments", new ArrayList<Map<String, String>>());
                    ((List<Map<String, Object>>) currentItem.get("conductors")).add(cond);
                    currentSub = cond;
                } else if (command.equals("PIN") && currentItem != null && (currentType.equals("CONNECTOR") || currentType.equals("COMPONENT"))) {
                    Map<String, Object> pin = new HashMap<>();
                    pin.put("name", parts[1]);
                    pin.put("parameters", new HashMap<String, String>());
                    ((List<Map<String, Object>>) currentItem.get("pins")).add(pin);
                    currentSub = pin;
                } else if (command.equals("ATTACH") && currentItem != null) {
                    Map<String, String> attach = new HashMap<>();
                    attach.put("from_comp", parts[1]);
                    attach.put("from_pin", parts[2]);
                    attach.put("to_comp", parts[3]);
                    attach.put("to_pin", parts.length > 4 ? parts[4] : "");
                    ((List<Map<String, String>>) (currentSub != null ? currentSub.get("attachments") : currentItem.get("attachments"))).add(attach);
                }
            }
        }
    }

    public void printProperties() {
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        System.out.println(gson.toJson(data));
    }

    public void write(String filepath) throws IOException {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filepath))) {
            for (Map.Entry<String, List<Map<String, Object>>> entry : data.entrySet()) {
                String category = entry.getKey();
                for (Map<String, Object> item : entry.getValue()) {
                    String type = category.substring(0, category.length() - 1).toUpperCase();
                    if (category.equals("wire_spools")) {
                        writer.write("NEW WIRE_SPOOL " + item.get("name") + "\n");
                    } else if (category.equals("cable_spools")) {
                        writer.write("NEW CABLE_SPOOL " + item.get("name") + " " + item.getOrDefault("num_conductors", "0") + "\n");
                    } else if (category.equals("connectors")) {
                        writer.write("NEW CONNECTOR " + item.get("name") + "\n");
                    } else if (category.equals("components")) {
                        writer.write("NEW COMPONENT " + item.get("name") + "\n");
                    } else if (category.equals("wires")) {
                        writer.write("NEW WIRE " + item.get("name") + " " + item.getOrDefault("spool", "") + "\n");
                    } else if (category.equals("cables")) {
                        writer.write("NEW CABLE " + item.get("name") + " " + item.getOrDefault("spool", "") + "\n");
                    } else if (category.equals("rails")) {
                        writer.write("NEW RAIL " + item.get("name") + "\n");
                    }
                    Map<String, String> params = (Map<String, String>) item.get("parameters");
                    for (Map.Entry<String, String> p : params.entrySet()) {
                        writer.write("PARAMETER " + p.getKey() + " " + p.getValue() + "\n");
                    }
                    List<Map<String, Object>> conductors = (List<Map<String, Object>>) item.get("conductors");
                    for (Map<String, Object> cond : conductors) {
                        writer.write("CONDUCTOR " + cond.get("id") + " " + cond.getOrDefault("name", "") + "\n");
                        params = (Map<String, String>) cond.get("parameters");
                        for (Map.Entry<String, String> p : params.entrySet()) {
                            writer.write("PARAMETER " + p.getKey() + " " + p.getValue() + "\n");
                        }
                        List<Map<String, String>> atts = (List<Map<String, String>>) cond.get("attachments");
                        for (Map<String, String> att : atts) {
                            writer.write("ATTACH " + att.get("from_comp") + " " + att.get("from_pin") + " " + att.get("to_comp") + " " + att.get("to_pin") + "\n");
                        }
                    }
                    List<Map<String, Object>> pins = (List<Map<String, Object>>) item.get("pins");
                    for (Map<String, Object> pin : pins) {
                        writer.write("PIN " + pin.get("name") + "\n");
                        params = (Map<String, String>) pin.get("parameters");
                        for (Map.Entry<String, String> p : params.entrySet()) {
                            writer.write("PARAMETER " + p.getKey() + " " + p.getValue() + "\n");
                        }
                    }
                    List<Map<String, String>> atts = (List<Map<String, String>>) item.get("attachments");
                    for (Map<String, String> att : atts) {
                        writer.write("ATTACH " + att.get("from_comp") + " " + att.get("from_pin") + " " + att.get("to_comp") + " " + att.get("to_pin") + "\n");
                    }
                    writer.write("\n");
                }
            }
        }
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     NWFHandler handler = new NWFHandler();
    //     handler.read("example.nwf");
    //     handler.printProperties();
    //     handler.write("output.nwf");
    // }
}

6. JavaScript Class for Handling .NWF Files

The following JavaScript class can parse, write (to string), and print properties from a .NWF file content (since JS typically handles strings or blobs in browser/node).

class NWFHandler {
  constructor() {
    this.data = {
      wire_spools: [], cable_spools: [], connectors: [], components: [],
      wires: [], cables: [], rails: []
    };
  }

  read(content) {
    const lines = content.split('\n').filter(line => line.trim() && !line.trim().startsWith('!') && !line.trim().startsWith('#'));
    let currentItem = null;
    let currentType = null;
    let currentSub = null;

    lines.forEach(line => {
      const parts = line.trim().split(/\s+/);
      const command = parts[0];
      if (command === 'NEW') {
        currentType = parts[1];
        const name = parts[2] || '';
        currentItem = { name, parameters: {}, conductors: [], pins: [], attachments: [] };
        if (currentType === 'CABLE_SPOOL') {
          currentItem.num_conductors = parts[3] || 0;
        } else if (currentType === 'WIRE' || currentType === 'CABLE') {
          currentItem.spool = parts[3] || '';
        }
        this.data[currentType.toLowerCase() + 's'].push(currentItem);
        currentSub = null;
      } else if (command === 'PARAMETER' && currentItem) {
        const key = parts[1];
        const value = parts.slice(2).join(' ');
        (currentSub || currentItem).parameters[key] = value;
      } else if (command === 'PARAMETERS' && currentItem) {
        (currentSub || currentItem).param_names = parts.slice(1);
      } else if (command === 'VALUES' && currentItem && (currentSub || currentItem).param_names) {
        const names = (currentSub || currentItem).param_names;
        delete (currentSub || currentItem).param_names;
        parts.slice(1).forEach((val, i) => {
          if (i < names.length) (currentSub || currentItem).parameters[names[i]] = val;
        });
      } else if (command === 'CONDUCTOR' && currentItem && (currentType === 'CABLE' || currentType === 'CABLE_SPOOL')) {
        const cond = { id: parts[1], name: parts[2] || '', parameters: {}, attachments: [] };
        currentItem.conductors.push(cond);
        currentSub = cond;
      } else if (command === 'PIN' && currentItem && (currentType === 'CONNECTOR' || currentType === 'COMPONENT')) {
        const pin = { name: parts[1], parameters: {} };
        currentItem.pins.push(pin);
        currentSub = pin;
      } else if (command === 'ATTACH' && currentItem) {
        const attach = { from_comp: parts[1], from_pin: parts[2], to_comp: parts[3], to_pin: parts[4] || '' };
        (currentSub || currentItem).attachments.push(attach);
      }
    });
  }

  printProperties() {
    console.log(JSON.stringify(this.data, null, 4));
  }

  write() {
    let output = '';
    Object.entries(this.data).forEach(([category, items]) => {
      items.forEach(item => {
        const type = category.slice(0, -1).toUpperCase();
        if (category === 'wire_spools') {
          output += `NEW WIRE_SPOOL ${item.name}\n`;
        } else if (category === 'cable_spools') {
          output += `NEW CABLE_SPOOL ${item.name} ${item.num_conductors || 0}\n`;
        } else if (category === 'connectors') {
          output += `NEW CONNECTOR ${item.name}\n`;
        } else if (category === 'components') {
          output += `NEW COMPONENT ${item.name}\n`;
        } else if (category === 'wires') {
          output += `NEW WIRE ${item.name} ${item.spool || ''}\n`;
        } else if (category === 'cables') {
          output += `NEW CABLE ${item.name} ${item.spool || ''}\n`;
        } else if (category === 'rails') {
          output += `NEW RAIL ${item.name}\n`;
        }
        Object.entries(item.parameters).forEach(([key, val]) => {
          output += `PARAMETER ${key} ${val}\n`;
        });
        item.conductors.forEach(cond => {
          output += `CONDUCTOR ${cond.id} ${cond.name || ''}\n`;
          Object.entries(cond.parameters).forEach(([key, val]) => {
            output += `PARAMETER ${key} ${val}\n`;
          });
          cond.attachments.forEach(att => {
            output += `ATTACH ${att.from_comp} ${att.from_pin} ${att.to_comp} ${att.to_pin}\n`;
          });
        });
        item.pins.forEach(pin => {
          output += `PIN ${pin.name}\n`;
          Object.entries(pin.parameters).forEach(([key, val]) => {
            output += `PARAMETER ${key} ${val}\n`;
          });
        });
        item.attachments.forEach(att => {
          output += `ATTACH ${att.from_comp} ${att.from_pin} ${att.to_comp} ${att.to_pin}\n`;
        });
        output += '\n';
      });
    });
    return output;
  }
}

// Example usage (in Node.js):
// const fs = require('fs');
// const handler = new NWFHandler();
// const content = fs.readFileSync('example.nwf', 'utf8');
// handler.read(content);
// handler.printProperties();
// const output = handler.write();
// fs.writeFileSync('output.nwf', output);

7. C++ Class for Handling .NWF Files

The following C++ class can open, parse, write, and print properties from a .NWF file. It uses standard libraries for file I/O and JSON-like output for printing.

#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <map>
#include <string>

struct Item {
    std::string name;
    std::map<std::string, std::string> parameters;
    std::vector<Item> conductors;
    std::vector<Item> pins;
    std::vector<std::map<std::string, std::string>> attachments;
    std::string spool; // For WIRE/CABLE
    std::string num_conductors; // For CABLE_SPOOL
};

class NWFHandler {
private:
    std::map<std::string, std::vector<Item>> data;

public:
    NWFHandler() {
        data["wire_spools"] = {};
        data["cable_spools"] = {};
        data["connectors"] = {};
        data["components"] = {};
        data["wires"] = {};
        data["cables"] = {};
        data["rails"] = {};
    }

    void read(const std::string& filepath) {
        std::ifstream file(filepath);
        if (!file.is_open()) return;
        std::string line;
        Item* currentItem = nullptr;
        std::string currentType;
        Item* currentSub = nullptr;
        while (std::getline(file, line)) {
            std::istringstream iss(line);
            std::string token;
            std::vector<std::string> parts;
            while (iss >> token) parts.push_back(token);
            if (parts.empty() || parts[0] == "!" || parts[0] == "#") continue;
            std::string command = parts[0];
            if (command == "NEW") {
                currentType = parts[1];
                Item newItem;
                newItem.name = (parts.size() > 2 ? parts[2] : "");
                if (currentType == "CABLE_SPOOL") {
                    newItem.num_conductors = (parts.size() > 3 ? parts[3] : "0");
                } else if (currentType == "WIRE" || currentType == "CABLE") {
                    newItem.spool = (parts.size() > 3 ? parts[3] : "");
                }
                data[currentType + "s"].push_back(newItem);
                currentItem = &data[currentType + "s"].back();
                currentSub = nullptr;
            } else if (command == "PARAMETER" && currentItem) {
                if (parts.size() < 3) continue;
                std::string key = parts[1];
                std::string value;
                for (size_t i = 2; i < parts.size(); ++i) value += (i > 2 ? " " : "") + parts[i];
                (currentSub ? currentSub : currentItem)->parameters[key] = value;
            } else if (command == "PARAMETERS" && currentItem) {
                std::vector<std::string> names(parts.begin() + 1, parts.end());
                (currentSub ? currentSub : currentItem)->temp_names = names; // Use a temp vector member (add to struct)
            } else if (command == "VALUES" && currentItem && !(currentSub ? currentSub : currentItem)->temp_names.empty()) {
                auto& names = (currentSub ? currentSub : currentItem)->temp_names;
                for (size_t i = 1; i < parts.size() && (i - 1) < names.size(); ++i) {
                    (currentSub ? currentSub : currentItem)->parameters[names[i - 1]] = parts[i];
                }
                names.clear();
            } else if (command == "CONDUCTOR" && currentItem && (currentType == "CABLE" || currentType == "CABLE_SPOOL")) {
                Item cond;
                cond.name = (parts.size() > 2 ? parts[2] : "");
                cond.parameters["id"] = parts[1]; // Store id as param for simplicity
                currentItem->conductors.push_back(cond);
                currentSub = &currentItem->conductors.back();
            } else if (command == "PIN" && currentItem && (currentType == "CONNECTOR" || currentType == "COMPONENT")) {
                Item pin;
                pin.name = parts[1];
                currentItem->pins.push_back(pin);
                currentSub = &currentItem->pins.back();
            } else if (command == "ATTACH" && currentItem) {
                std::map<std::string, std::string> attach;
                attach["from_comp"] = parts[1];
                attach["from_pin"] = parts[2];
                attach["to_comp"] = parts[3];
                attach["to_pin"] = (parts.size() > 4 ? parts[4] : "");
                (currentSub ? currentSub : currentItem)->attachments.push_back(attach);
            }
        }
        file.close();
    }

    void printProperties() const {
        // Simple JSON-like output
        for (const auto& entry : data) {
            std::cout << "\"" << entry.first << "\": [\n";
            for (const auto& item : entry.second) {
                std::cout << "  {\n";
                std::cout << "    \"name\": \"" << item.name << "\",\n";
                if (!item.spool.empty()) std::cout << "    \"spool\": \"" << item.spool << "\",\n";
                if (!item.num_conductors.empty()) std::cout << "    \"num_conductors\": \"" << item.num_conductors << "\",\n";
                std::cout << "    \"parameters\": {\n";
                for (const auto& p : item.parameters) {
                    std::cout << "      \"" << p.first << "\": \"" << p.second << "\",\n";
                }
                std::cout << "    },\n";
                std::cout << "    \"conductors\": [\n";
                for (const auto& cond : item.conductors) {
                    std::cout << "      { \"name\": \"" << cond.name << "\", \"parameters\": {";
                    for (const auto& p : cond.parameters) std::cout << "\"" << p.first << "\": \"" << p.second << "\", ";
                    std::cout << "}, \"attachments\": [";
                    for (const auto& att : cond.attachments) {
                        std::cout << "{ \"from_comp\": \"" << att.at("from_comp") << "\", \"from_pin\": \"" << att.at("from_pin") << "\", \"to_comp\": \"" << att.at("to_comp") << "\", \"to_pin\": \"" << att.at("to_pin") << "\" },";
                    }
                    std::cout << "] },\n";
                }
                std::cout << "    ],\n";
                std::cout << "    \"pins\": [\n";
                for (const auto& pin : item.pins) {
                    std::cout << "      { \"name\": \"" << pin.name << "\", \"parameters\": {";
                    for (const auto& p : pin.parameters) std::cout << "\"" << p.first << "\": \"" << p.second << "\", ";
                    std::cout << "} },\n";
                }
                std::cout << "    ],\n";
                std::cout << "    \"attachments\": [\n";
                for (const auto& att : item.attachments) {
                    std::cout << "      { \"from_comp\": \"" << att.at("from_comp") << "\", \"from_pin\": \"" << att.at("from_pin") << "\", \"to_comp\": \"" << att.at("to_comp") << "\", \"to_pin\": \"" << att.at("to_pin") << "\" },\n";
                }
                std::cout << "    ]\n  },\n";
            }
            std::cout << "],\n";
        }
    }

    void write(const std::string& filepath) const {
        std::ofstream file(filepath);
        if (!file.is_open()) return;
        for (const auto& entry : data) {
            std::string category = entry.first;
            for (const auto& item : entry.second) {
                std::string type = category.substr(0, category.size() - 1);
                std::transform(type.begin(), type.end(), type.begin(), ::toupper);
                if (category == "wire_spools") {
                    file << "NEW WIRE_SPOOL " << item.name << "\n";
                } else if (category == "cable_spools") {
                    file << "NEW CABLE_SPOOL " << item.name << " " << item.num_conductors << "\n";
                } else if (category == "connectors") {
                    file << "NEW CONNECTOR " << item.name << "\n";
                } else if (category == "components") {
                    file << "NEW COMPONENT " << item.name << "\n";
                } else if (category == "wires") {
                    file << "NEW WIRE " << item.name << " " << item.spool << "\n";
                } else if (category == "cables") {
                    file << "NEW CABLE " << item.name << " " << item.spool << "\n";
                } else if (category == "rails") {
                    file << "NEW RAIL " << item.name << "\n";
                }
                for (const auto& p : item.parameters) {
                    file << "PARAMETER " << p.first << " " << p.second << "\n";
                }
                for (const auto& cond : item.conductors) {
                    file << "CONDUCTOR " << cond.parameters.at("id") << " " << cond.name << "\n";
                    for (const auto& p : cond.parameters) {
                        if (p.first != "id") file << "PARAMETER " << p.first << " " << p.second << "\n";
                    }
                    for (const auto& att : cond.attachments) {
                        file << "ATTACH " << att.at("from_comp") << " " << att.at("from_pin") << " " << att.at("to_comp") << " " << att.at("to_pin") << "\n";
                    }
                }
                for (const auto& pin : item.pins) {
                    file << "PIN " << pin.name << "\n";
                    for (const auto& p : pin.parameters) {
                        file << "PARAMETER " << p.first << " " << p.second << "\n";
                    }
                }
                for (const auto& att : item.attachments) {
                    file << "ATTACH " << att.at("from_comp") << " " << att.at("from_pin") << " " << att.at("to_comp") << " " << att.at("to_pin") << "\n";
                }
                file << "\n";
            }
        }
        file.close();
    }
};

// Note: Add std::vector<std::string> temp_names; to Item struct for PARAMETERS handling.
// Example usage:
// int main() {
//     NWFHandler handler;
//     handler.read("example.nwf");
//     handler.printProperties();
//     handler.write("output.nwf");
//     return 0;
// }