Browse Source

modtool import

Martin Kunz 9 months ago
commit
866f70f01a

+ 14 - 0
.eslintrc.cjs

@@ -0,0 +1,14 @@
+/* eslint-env node */
+require('@rushstack/eslint-patch/modern-module-resolution')
+
+module.exports = {
+  root: true,
+  'extends': [
+    'plugin:vue/vue3-essential',
+    'eslint:recommended',
+    '@vue/eslint-config-typescript'
+  ],
+  parserOptions: {
+    ecmaVersion: 'latest'
+  }
+}

+ 28 - 0
.gitignore

@@ -0,0 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
+}

+ 46 - 0
README.md

@@ -0,0 +1,46 @@
+# modtool
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
+
+## Type Support for `.vue` Imports in TS
+
+TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
+
+If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
+
+1. Disable the built-in TypeScript Extension
+    1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
+    2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
+2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vitejs.dev/config/).
+
+## Project Setup
+
+```sh
+npm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+npm run dev
+```
+
+### Type-Check, Compile and Minify for Production
+
+```sh
+npm run build
+```
+
+### Lint with [ESLint](https://eslint.org/)
+
+```sh
+npm run lint
+```

+ 0 - 0
README.me


+ 1 - 0
env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <link rel="icon" href="/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Vite App</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 5 - 0
jsconfig.json

@@ -0,0 +1,5 @@
+{
+    "compilerOptions": {
+      "allowJs": true
+    }
+ }

File diff suppressed because it is too large
+ 6707 - 0
package-lock.json


+ 34 - 0
package.json

@@ -0,0 +1,34 @@
+{
+  "name": "modtool",
+  "version": "0.0.0",
+  "private": true,
+  "scripts": {
+    "dev": "vite",
+    "build": "run-p type-check build-only",
+    "preview": "vite preview",
+    "build-only": "vite build",
+    "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
+  },
+  "dependencies": {
+    "axios": "^1.4.0",
+    "fast-xml-parser": "^4.2.6",
+    "vue": "^3.3.4",
+    "vue-router": "^4.2.4"
+  },
+  "devDependencies": {
+    "@rushstack/eslint-patch": "^1.3.2",
+    "@tsconfig/node18": "^18.2.0",
+    "@types/node": "^18.17.1",
+    "@types/xml2js": "^0.4.11",
+    "@vitejs/plugin-vue": "^4.2.3",
+    "@vue/eslint-config-typescript": "^11.0.3",
+    "@vue/tsconfig": "^0.4.0",
+    "eslint": "^8.45.0",
+    "eslint-plugin-vue": "^9.15.1",
+    "npm-run-all": "^4.1.5",
+    "typescript": "~5.1.6",
+    "vite": "^4.4.6",
+    "vue-tsc": "^1.8.6"
+  }
+}

BIN
public/favicon.ico


File diff suppressed because it is too large
+ 5024 - 0
public/nodesets/Opc.Ua.Di.NodeSet2.xml


File diff suppressed because it is too large
+ 1663 - 0
public/nodesets/Opc.Ua.Ia.NodeSet2.xml


File diff suppressed because it is too large
+ 5613 - 0
public/nodesets/Opc.Ua.MachineTool.Nodeset2.xml


File diff suppressed because it is too large
+ 1579 - 0
public/nodesets/Opc.Ua.Machinery.NodeSet2.xml


File diff suppressed because it is too large
+ 76352 - 0
public/nodesets/Opc.Ua.NodeSet2.xml


File diff suppressed because it is too large
+ 4551 - 0
public/nodesets/emco_machine_tools.xml


+ 50 - 0
src/AddressSpace.ts

@@ -0,0 +1,50 @@
+import { UANodeSet } from "./UANodeSet";
+import { UABaseNode } from "./UABaseNode";
+import { NamespaceTable } from "./NameSpaceTable";
+
+export class AddressSpace{
+
+    nodeMap:  Map<string, UABaseNode>;
+
+    constructor(public nodesets: UANodeSet[]) {
+        this.reIndex(nodesets);
+        this.nodeMap=this.buildNodeMap(nodesets);
+        this.resolveChildren(nodesets, this.nodeMap);
+    }
+
+    private reIndex(nodesets:UANodeSet[]) {
+        const nst=new NamespaceTable();
+        for(const nodeset of nodesets) {
+            nodeset.reIndex(nst);
+        }
+    }
+    
+    private buildNodeMap(nodesets:UANodeSet[]) {
+        const nm=new Map<string, UABaseNode>();
+        for(const nodeset of nodesets) {
+            for(const node of nodeset.nodes) {
+                nm.set(node.nodeId.toString(), node);
+            }
+        }
+        return nm;
+    }
+
+    private resolveChildren(nodesets:UANodeSet[], nm:Map<string, UABaseNode>) {
+        for(const nodeset of nodesets) {
+            nodeset.resolveChildren(nm);
+        }
+    }
+
+    public findNode(nodeId: string):UABaseNode|undefined {
+        return this.nodeMap.get(nodeId)
+    }
+
+    static async load(files: string[]) {
+        const sets:UANodeSet[]= [];
+        for(const file of files) {
+            const ns=await UANodeSet.load(file);
+            sets.push(ns);
+        }
+        return new AddressSpace(sets);
+    }
+}

+ 80 - 0
src/App.vue

@@ -0,0 +1,80 @@
+<script setup lang="ts">
+import { RouterLink } from 'vue-router'
+
+</script>
+
+<template>
+  <header>
+    <div class="wrapper">
+      <nav>
+        <RouterLink to="/">Home</RouterLink>
+        <RouterLink to="/about">About</RouterLink>
+      </nav>
+    </div>
+  </header>
+  <RouterView />
+</template>
+
+<style scoped>
+header {
+  line-height: 1.5;
+  max-height: 100vh;
+}
+
+.logo {
+  display: block;
+  margin: 0 auto 2rem;
+}
+
+nav {
+  width: 100%;
+  font-size: 12px;
+  text-align: center;
+  margin-top: 2rem;
+}
+
+nav a.router-link-exact-active {
+  color: var(--color-text);
+}
+
+nav a.router-link-exact-active:hover {
+  background-color: transparent;
+}
+
+nav a {
+  display: inline-block;
+  padding: 0 1rem;
+  border-left: 1px solid var(--color-border);
+}
+
+nav a:first-of-type {
+  border: 0;
+}
+
+@media (min-width: 1024px) {
+  header {
+    display: flex;
+    place-items: center;
+    padding-right: calc(var(--section-gap) / 2);
+  }
+
+  .logo {
+    margin: 0 2rem 0 0;
+  }
+
+  header .wrapper {
+    display: flex;
+    place-items: flex-start;
+    flex-wrap: wrap;
+  }
+
+  nav {
+    text-align: left;
+    margin-left: -1rem;
+    font-size: 1rem;
+
+    padding: 1rem 0;
+    margin-top: 1rem;
+  }
+}
+</style>

+ 36 - 0
src/NameSpaceTable.ts

@@ -0,0 +1,36 @@
+import { BiMap } from "./util/bi-map";
+
+
+export class NamespaceTable {
+    OPC_UA = "http://opcfoundation.org/UA/";
+    nsMap = new BiMap<number, string>();
+
+    constructor() {
+        this.nsMap.set(0, this.OPC_UA)
+    }
+
+    addUri(uri: string):number|undefined {
+        if(this.nsMap.hasValue(uri)) {
+            return this.nsMap.getFromValue(uri);
+        } else {
+            let index=1;
+            while (this.nsMap.hasKey(index)) {
+                index = index + 1;
+            }
+            this.nsMap.set(index, uri);
+            return index;
+        }
+    }
+
+    putUri(uri: string, index: number): void {
+        this.nsMap.set(index, uri);
+    }
+
+    getUri(index: number): string|undefined {
+        return this.nsMap.get(index);
+    }
+
+    getIndex(uri: string) : number|undefined {
+        return this.nsMap.getFromValue(uri);
+    }
+}

+ 126 - 0
src/NodeId.ts

@@ -0,0 +1,126 @@
+import { assert } from "./util/assert";
+
+
+export class NodeId  {
+    public identifierType: NodeIdType;
+    public value: number | string;
+    public namespace: number;
+    constructor(identifierType?: NodeIdType | null, value?: number | string, namespace?: number) {
+        if (identifierType === null || identifierType === undefined) {
+            this.identifierType = NodeIdType.NUMERIC;
+            this.value = 0;
+            this.namespace = 0;
+            return;
+        }
+
+        this.identifierType = identifierType;
+        this.value = value || this.defaultValue(identifierType);
+        this.namespace = namespace || 0;
+
+        // namespace shall be a UInt16
+        assert(this.namespace >= 0 && this.namespace <= 0xffff, "NodeId: invalid namespace value");
+        assert(this.identifierType !== NodeIdType.NUMERIC || (this.value !== null && this.value as number >= 0 && this.value as number <= 0xffffffff));
+        assert(this.identifierType !== NodeIdType.STRING || typeof this.value === "string", "cannot  empty string");
+
+    }
+
+    defaultValue(identifierType: NodeIdType): string | number {
+        switch (identifierType) {
+            case NodeIdType.STRING: return "";
+            case NodeIdType.NUMERIC: return 0;
+        }
+    }
+
+    public toString(): string {
+        let str;
+        switch (this.identifierType) {
+            case NodeIdType.NUMERIC:
+                str = "ns=" + this.namespace + ";i=" + this.value;
+                break;
+            case NodeIdType.STRING:
+                str = "ns=" + this.namespace + ";s=" + this.value;
+                break;
+        }
+        return str;
+    }
+
+
+
+/**
+ * Convert a value into a nodeId:
+ * @class opcua
+ * @method coerceNodeId
+ * @static
+ *
+ * @description:
+ *    - if nodeId is a string of form : "i=1234"  => nodeId({value=1234, identifierType: NodeIdType.NUMERIC})
+ *    - if nodeId is a string of form : "s=foo"   => nodeId({value="foo", identifierType: NodeIdType.STRING})
+ *    - if nodeId is a string of form : "b=ABCD=" => nodeId({value=decodeBase64("ABCD="), identifierType: NodeIdType.BYTESTRING})
+ *    - if nodeId is a {@link NodeId} :  coerceNodeId returns value
+ * @param value
+ * @param namespace {number}
+ */
+// eslint-disable-next-line max-statements
+ static coerceNodeId(value: unknown, namespace?: number): NodeId {
+    const regexNamespaceI = /ns=([0-9]+);i=([0-9]+)/;
+    const regexNamespaceS = /ns=([0-9]+);s=(.*)/;
+    let matches;
+    let twoFirst;
+    if (value instanceof NodeId) {
+        return value;
+    }
+
+    value = value || 0;
+    namespace = namespace || 0;
+
+    let identifierType = NodeIdType.NUMERIC;
+
+    if (typeof value === "string") {
+        identifierType = NodeIdType.STRING;
+
+        twoFirst = value.substring(0, 2);
+        if (twoFirst === "i=") {
+            identifierType = NodeIdType.NUMERIC;
+            value = parseInt(value.substring(2), 10);
+        } else if (twoFirst === "s=") {
+            identifierType = NodeIdType.STRING;
+            value = value.substring(2);
+
+        } else if ((matches = regexNamespaceI.exec(value)) !== null) {
+            identifierType = NodeIdType.NUMERIC;
+            namespace = parseInt(matches[1], 10);
+            value = parseInt(matches[2], 10);
+        } else if ((matches = regexNamespaceS.exec(value)) !== null) {
+            identifierType = NodeIdType.STRING;
+            namespace = parseInt(matches[1], 10);
+            value = matches[2];
+        } else {
+            throw new Error("String cannot be coerced to a nodeId : " + value);
+        }
+    } else if (value instanceof Object) {
+        // it could be a Enum or a NodeId Like object
+        const tmp = value as any;
+        value = tmp.value;
+        namespace = namespace || tmp.namespace;
+        identifierType = tmp.identifierType || identifierType;
+        return new NodeId(identifierType, value as any, namespace);
+    }
+    return new NodeId(identifierType, value as any, namespace);
+}
+}
+
+export enum NodeIdType {
+    /**
+     * @static
+     * @property NUMERIC
+     * @default 0x1
+     */
+    NUMERIC = 0x01,
+    /**
+     * @static
+     * @property STRING
+     * @default 0x2
+     */
+    STRING = 0x02,
+}
+

+ 57 - 0
src/UABaseNode.ts

@@ -0,0 +1,57 @@
+import type { NamespaceTable } from "./NameSpaceTable";
+import { NodeId } from "./NodeId";
+import { ReferencyType, UAReference } from "./UAReference";
+import { assert } from "./util/assert";
+
+export class UABaseNode {
+    children: UABaseNode[]=[];
+    constructor(public nodeId: NodeId,
+                public browseName: string,
+                public displayName: string,
+                public references: UAReference[]) {
+    }
+
+    reIndex(nst: NamespaceTable, onst: NamespaceTable) {
+        const nsName=onst.getUri(this.nodeId.namespace);
+        assert(nsName!=undefined)
+        const newIndex=nst.getIndex(nsName);
+        assert(newIndex!=undefined)
+        this.nodeId.namespace=newIndex;
+
+        for(const uaref of this.references) {
+            uaref.reIndex(nst, onst);
+        }
+    }
+
+    resolveChildren(nm: Map<string, UABaseNode>) {
+        for(const ref of this.references) {
+            const node=nm.get(ref.ref.toString()) 
+            if(node==undefined) {
+                //TODO: parse all types/nodes
+                continue;
+            }
+            if(ref.referenceType==ReferencyType.HasComponent && ref.isForward) {
+                this.children.push(node);
+            }
+            if(ref.referenceType==ReferencyType.HasComponent && ref.isForward==false) {
+                node.children.push(this);
+            }
+            if(ref.referenceType==ReferencyType.Organizes && ref.isForward) {
+                this.children.push(node);
+            }
+            if(ref.referenceType==ReferencyType.Organizes && ref.isForward==false) {
+                node.children.push(this);
+            }
+        }
+    }
+
+    static  parse(xmlObject: any): UABaseNode{
+        const xmlReferences=xmlObject['References'];
+        const references:UAReference[]=[];
+        for(const xmlref of xmlReferences.Reference) {
+            references.push(UAReference.parse(xmlref));
+        }
+        const ua=new UABaseNode(NodeId.coerceNodeId(xmlObject['@_NodeId']), xmlObject['@_BrowseName'], xmlObject['DisplayName'], references);
+        return ua;
+    }
+}

+ 61 - 0
src/UANodeSet.ts

@@ -0,0 +1,61 @@
+import {XMLParser, type X2jOptions} from 'fast-xml-parser';
+import axios from 'axios';
+import { UAObject } from './UAObject';
+import type { UABaseNode } from './UABaseNode';
+import { UAVariable } from './UAVariable';
+import { NamespaceTable } from './NameSpaceTable';
+
+export class UANodeSet {
+
+    constructor(public nodes: UABaseNode[],
+                public nameSpaceTable: NamespaceTable) {
+    }
+
+    reIndex(nst: NamespaceTable) {
+        for(const value of this.nameSpaceTable.nsMap.getValues()) {
+            nst.addUri(value);
+        }
+        for(const node of this.nodes) {
+            node.reIndex(nst, this.nameSpaceTable);
+        }
+    }
+
+    resolveChildren(nm: Map<string, UABaseNode>) {
+        for(const node of this.nodes) {
+            node.resolveChildren(nm);
+        }    
+    }
+
+    static async load(url: string) {
+        const xml= (await axios.get(url)).data;     
+        const parseOptions:Partial<X2jOptions>={
+            ignoreAttributes: false,
+            // eslint-disable-next-line @typescript-eslint/no-unused-vars
+            isArray: (name, jpath, isLeafNode, isAttribute):boolean => { 
+                if(jpath=='UANodeSet.NamespaceUris.Uri') return true;
+                if(jpath=='UANodeSet.UAObject.References.Reference') return true;
+                if(jpath=='UANodeSet.UAVariable.References.Reference') return true;
+                return false;
+            }
+        }
+        const parser = new XMLParser(parseOptions);
+        const jObj = parser.parse(xml);
+        const nodes:UABaseNode[]=[];
+        const uaObjects=jObj['UANodeSet']['UAObject'];
+        for(const uaObject of uaObjects) {
+            nodes.push(UAObject.parse(uaObject));
+        }
+        const uaVariables=jObj['UANodeSet']['UAVariable'];
+        for(const uaVariable of uaVariables) {
+            nodes.push(UAVariable.parse(uaVariable));
+        }
+        const uaNamespaceUris=jObj['UANodeSet']['NamespaceUris'];
+        const nst=new NamespaceTable();
+        if(uaNamespaceUris) {
+            for(const nsUri of uaNamespaceUris['Uri']) {
+                nst.addUri(nsUri)
+            }
+        }
+        return new UANodeSet(nodes, nst);
+    }
+}

+ 17 - 0
src/UAObject.ts

@@ -0,0 +1,17 @@
+import type { NodeId } from "./NodeId";
+import { UABaseNode } from "./UABaseNode";
+import { UAReference } from "./UAReference";
+
+export class UAObject extends UABaseNode {
+    constructor(public nodeId: NodeId,
+                public browseName: string,
+                public displayName: string,
+                public references: UAReference[]) {
+                    super(nodeId, browseName, displayName, references);
+    }
+
+    static  parse(uaObject: any): UAObject{
+        const bn=super.parse(uaObject)
+        return new UAObject(bn.nodeId, bn.browseName, bn.displayName, bn.references);
+    }
+}

+ 39 - 0
src/UAReference.ts

@@ -0,0 +1,39 @@
+import type { NamespaceTable } from "./NameSpaceTable";
+import { NodeId } from "./NodeId";
+import { assert } from "./util/assert";
+
+export class UAReference {
+    constructor(public referenceType: ReferencyType,
+                public ref: NodeId,
+                public isForward: boolean) {
+    }
+
+    reIndex(nst: NamespaceTable, onst: NamespaceTable) {
+        const nsName=onst.getUri(this.ref.namespace);
+        assert(nsName!=undefined)
+        const newIndex=nst.getIndex(nsName);
+        assert(newIndex!=undefined)
+        this.ref.namespace=newIndex;    
+    }
+
+    static parse(uaReference: any): UAReference {
+        return new UAReference(ReferencyType[uaReference['@_ReferenceType'] as keyof typeof ReferencyType], 
+                                NodeId.coerceNodeId(uaReference['#text']), 
+                                uaReference['@_IsForward']!="false");
+    }
+}
+
+
+export enum ReferencyType {
+    HasComponent  = 1,
+    HasProperty =2,
+    Organizes =3,
+    HasOrderedComponent =4,
+    HasModellingRule =5,
+    HasTypeDefinition =6,
+    HasEncoding =7,
+    HasDescription =8,
+    GeneratesEvent =9,
+    AlwaysGeneratesEvent=10,
+    HasNotifier=11
+}

+ 17 - 0
src/UAVariable.ts

@@ -0,0 +1,17 @@
+import type { NodeId } from "./NodeId";
+import { UABaseNode } from "./UABaseNode";
+import { UAReference } from "./UAReference";
+
+export class UAVariable extends UABaseNode {
+    constructor(public nodeId: NodeId,
+                public browseName: string,
+                public displayName: string,
+                public references: UAReference[]) {
+                    super(nodeId, browseName, displayName, references);
+    }
+
+    static  parse(uaObject: any): UAVariable{
+        const bn=super.parse(uaObject)
+        return new UAVariable(bn.nodeId, bn.browseName, bn.displayName, bn.references);
+    }
+}

+ 73 - 0
src/assets/base.css

@@ -0,0 +1,73 @@
+/* color palette from <https://github.com/vuejs/theme> */
+:root {
+  --vt-c-white: #ffffff;
+  --vt-c-white-soft: #f8f8f8;
+  --vt-c-white-mute: #f2f2f2;
+
+  --vt-c-black: #181818;
+  --vt-c-black-soft: #222222;
+  --vt-c-black-mute: #282828;
+
+  --vt-c-indigo: #2c3e50;
+
+  --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
+  --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
+  --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
+  --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
+
+  --vt-c-text-light-1: var(--vt-c-indigo);
+  --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
+  --vt-c-text-dark-1: var(--vt-c-white);
+  --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
+}
+
+/* semantic color variables for this project */
+:root {
+  --color-background: var(--vt-c-white);
+  --color-background-soft: var(--vt-c-white-soft);
+  --color-background-mute: var(--vt-c-white-mute);
+
+  --color-border: var(--vt-c-divider-light-2);
+  --color-border-hover: var(--vt-c-divider-light-1);
+
+  --color-heading: var(--vt-c-text-light-1);
+  --color-text: var(--vt-c-text-light-1);
+
+  --section-gap: 160px;
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --color-background: var(--vt-c-black);
+    --color-background-soft: var(--vt-c-black-soft);
+    --color-background-mute: var(--vt-c-black-mute);
+
+    --color-border: var(--vt-c-divider-dark-2);
+    --color-border-hover: var(--vt-c-divider-dark-1);
+
+    --color-heading: var(--vt-c-text-dark-1);
+    --color-text: var(--vt-c-text-dark-2);
+  }
+}
+
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+  margin: 0;
+  font-weight: normal;
+}
+
+body {
+  min-height: 100vh;
+  color: var(--color-text);
+  background: var(--color-background);
+  transition: color 0.5s, background-color 0.5s;
+  line-height: 1.6;
+  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
+    Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+  font-size: 15px;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}

+ 1 - 0
src/assets/logo.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

+ 35 - 0
src/assets/main.css

@@ -0,0 +1,35 @@
+@import './base.css';
+
+#app {
+  max-width: 1280px;
+  margin: 0 auto;
+  padding: 2rem;
+
+  font-weight: normal;
+}
+
+a,
+.green {
+  text-decoration: none;
+  color: hsla(160, 100%, 37%, 1);
+  transition: 0.4s;
+}
+
+@media (hover: hover) {
+  a:hover {
+    background-color: hsla(160, 100%, 37%, 0.2);
+  }
+}
+
+@media (min-width: 1024px) {
+  body {
+    display: flex;
+    place-items: center;
+  }
+
+  #app {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    padding: 0 2rem;
+  }
+}

+ 30 - 0
src/components/TheModeler.vue

@@ -0,0 +1,30 @@
+<script setup lang="ts">
+import { AddressSpace } from '@/AddressSpace';
+import type { UABaseNode } from '@/UABaseNode';
+import TreeItem from './TreeItem.vue'
+
+
+
+async function load(): Promise<UABaseNode|undefined> {
+  let files=['nodesets/Opc.Ua.NodeSet2.xml',
+             'nodesets/Opc.Ua.Di.NodeSet2.xml',
+             'nodesets/Opc.Ua.Ia.NodeSet2.xml',
+             'nodesets/Opc.Ua.Machinery.NodeSet2.xml',
+             'nodesets/Opc.Ua.MachineTool.Nodeset2.xml',
+             'nodesets/emco_machine_tools.xml']
+  const start = new Date().getTime();
+  const as=await AddressSpace.load(files);
+  let elapsed = new Date().getTime() - start;
+  console.log(elapsed);
+  const rootNode=as.findNode("ns=0;i=84");
+  return rootNode;
+}
+
+const rootNode=await load();
+</script>
+
+<template>
+  <ul>
+    <TreeItem class="item" :model="rootNode"></TreeItem>
+  </ul>
+</template>

+ 34 - 0
src/components/TreeItem.vue

@@ -0,0 +1,34 @@
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+
+const props = defineProps({
+  model: Object
+})
+
+const isOpen = ref(false)
+const isFolder = computed(() => {
+  return props.model.children && props.model.children.length
+})
+
+function toggle() {
+  isOpen.value = !isOpen.value
+}
+</script>
+
+<template>
+  <li>
+    <div
+      :class="{ bold: isFolder }"
+      @click="toggle">
+      {{ model.displayName }}
+      <span v-if="isFolder">[{{ isOpen ? '-' : '+' }}]</span>
+    </div>
+    <ul v-show="isOpen" v-if="isFolder">
+      <TreeItem
+        class="item"
+        v-for="model in model.children"
+        :model="model">
+      </TreeItem>
+    </ul>
+  </li>
+</template>

+ 11 - 0
src/main.ts

@@ -0,0 +1,11 @@
+import './assets/main.css'
+
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+
+const app = createApp(App)
+
+app.use(router)
+
+app.mount('#app')

+ 23 - 0
src/router/index.ts

@@ -0,0 +1,23 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import HomeView from '../views/HomeView.vue'
+
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: '/',
+      name: 'home',
+      component: HomeView
+    },
+    {
+      path: '/about',
+      name: 'about',
+      // route level code-splitting
+      // this generates a separate chunk (About.[hash].js) for this route
+      // which is lazy-loaded when the route is visited.
+      component: () => import('../views/AboutView.vue')
+    }
+  ]
+})
+
+export default router

+ 3 - 0
src/util/assert.ts

@@ -0,0 +1,3 @@
+export function assert(condition: unknown, msg?: string): asserts condition {
+    if (condition === false) throw new Error(msg)
+  }

+ 133 - 0
src/util/bi-map.ts

@@ -0,0 +1,133 @@
+/*
+ * Created by Trevor Sears <trevor@trevorsears.com> (https://trevorsears.com/).
+ * 1:04 PM -- September 14th, 2019.
+ * Project: @jsdsl/bimap
+ * 
+ * @jsdsl/bimap - A bidirectional map written in TypeScript.
+ * Copyright (C) 2021 Trevor Sears
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * 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 General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * A bidirectional map written in TypeScript.
+ *
+ * @author Trevor Sears <trevorsears.main@gmail.com>
+ * @version v1.0.0
+ * @since v0.1.0
+ */
+export class BiMap<K = any, V = any> {
+	
+	private primaryMap: Map<K, V>;
+	
+	private secondaryMap: Map<V, K>;
+	
+	public constructor() {
+	
+		this.primaryMap = new Map<K, V>();
+		this.secondaryMap = new Map<V, K>();
+	
+	}
+
+	public getValues(): IterableIterator<V> {
+		return this.primaryMap.values();
+	}
+	
+	public get(key: K): V | undefined {
+		
+		return this.getFromKey(key);
+		
+	}
+	
+	public set(key: K, value: V): void {
+		
+		this.setFromKey(key, value);
+		
+	}
+	
+	public getFromKey(key: K): V | undefined {
+		
+		return this.primaryMap.get(key);
+		
+	}
+	
+	public getFromValue(value: V): K | undefined {
+		
+		return this.secondaryMap.get(value);
+		
+	}
+	
+	public setFromKey(key: K, value: V): void {
+	
+		this.primaryMap.set(key, value);
+		this.secondaryMap.set(value, key);
+	
+	}
+	
+	public setFromValue(value: V, key: K): void {
+		
+		this.setFromKey(key, value);
+		
+	}
+	
+	public removeByKey(key: K): V | undefined {
+	
+		if (this.primaryMap.has(key)) {
+			
+			const value: V = this.primaryMap.get(key) as V;
+			
+			this.primaryMap.delete(key);
+			this.secondaryMap.delete(value);
+			
+			return value;
+			
+		} else return undefined;
+	
+	}
+	
+	public removeByValue(value: V): K | undefined {
+		
+		if (this.secondaryMap.has(value)) {
+			
+			const key: K = this.secondaryMap.get(value) as K;
+			
+			this.primaryMap.delete(key);
+			this.secondaryMap.delete(value);
+			
+			return key;
+			
+		} else return undefined;
+	
+	}
+	
+	public hasKey(key: K): boolean {
+	
+		return this.primaryMap.has(key);
+	
+	}
+	
+	public hasValue(value: V): boolean {
+	
+		return this.secondaryMap.has(value);
+	
+	}
+	
+	public clear(): void {
+		
+		this.primaryMap.clear();
+		this.secondaryMap.clear();
+		
+	}
+	
+}

+ 15 - 0
src/views/AboutView.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="about">
+    <h1>This is an about page</h1>
+  </div>
+</template>
+
+<style>
+@media (min-width: 1024px) {
+  .about {
+    min-height: 100vh;
+    display: flex;
+    align-items: center;
+  }
+}
+</style>

+ 14 - 0
src/views/HomeView.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+import TheWelcome from '../components/TheModeler.vue'
+</script>
+
+<template>
+  <main>
+    <Suspense>
+      <TheWelcome />
+    <template #fallback>
+      Loading...
+    </template>
+  </Suspense>
+  </main>
+</template>

+ 12 - 0
tsconfig.app.json

@@ -0,0 +1,12 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "jsconfig.json"],
+  "exclude": ["src/**/__tests__/*"],
+  "compilerOptions": {
+    "composite": true,
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  }
+}

+ 11 - 0
tsconfig.json

@@ -0,0 +1,11 @@
+{
+  "files": [],
+  "references": [
+    {
+      "path": "./tsconfig.node.json"
+    },
+    {
+      "path": "./tsconfig.app.json"
+    }
+  ]
+}

+ 16 - 0
tsconfig.node.json

@@ -0,0 +1,16 @@
+{
+  "extends": "@tsconfig/node18/tsconfig.json",
+  "include": [
+    "vite.config.*",
+    "vitest.config.*",
+    "cypress.config.*",
+    "nightwatch.conf.*",
+    "playwright.config.*"
+  ],
+  "compilerOptions": {
+    "composite": true,
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "types": ["node"]
+  }
+}

+ 16 - 0
vite.config.ts

@@ -0,0 +1,16 @@
+import { fileURLToPath, URL } from 'node:url'
+
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [
+    vue(),
+  ],
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url))
+    }
+  }
+})