Compare commits

...
3 Commits
Author SHA1 Message Date
Christoph 91b86f29cb Merge pull request 'Add TclOO class completions, signature help and inlay hints' (#39) from add_class_support into main
build_and_puplish.yml / build_and_publish (release) Successful in 39s
2026-09-11 21:39:34 +00:00
Christoph Brandau f88a50d4ab feat(tcloo): add document-local TclOO completions, signature help and hints
Add conservative, document-local TclOO type inference and tooling so the server
can offer method completions, signature help, and inlay parameter hints for
statically resolvable TclOO receivers (including `new`/`create`, `my`, and
simple return-chains). Also surface class names as completion items and emit
semantic tokens for class declarations/references.

Notable changes:
- New tcloo_* tools: completion, symbols, and argument parsing; integrated into
  on_completion, signature_help, inlay hint generation and semantic token
  highlighting. Completions are returned early when an OO receiver context is
  detected.
- Use a completion-friendly parser fallback when the main AST fails (TclSyntaxError)
  so editing-in-progress code still yields useful completions.
- Add CompletionItemKind.Class to command kinds, exclude class items from the
  poco completion name cache, and include new unit tests for the TclOO helpers.
2026-09-11 23:38:42 +02:00
Christoph c28836933c Update version to 2026.9.300 2026-09-10 08:31:25 +00:00
13 changed files with 1008 additions and 43 deletions
+3
View File
@@ -1,5 +1,8 @@
## Unreleased
- Add document-local TclOO method completion for `new`/`create` instances, `my`, and statically inferred return chains
- Suggest TclOO class names as classes and semantically highlight their declarations and calls
- Show signature help and parameter inlay hints for resolved TclOO methods and constructors, including optional and variadic arguments
- Add incoming and outgoing call hierarchy for custom TCL procedures and MOM event handlers
- Add document highlights for procedure and variable occurrences
- Make completion context-aware and prioritize local, current-file, workspace, and built-in symbols
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "nx-post-support",
"displayName": "NX Postprocessor Support",
"description": "VS Code extension for NX CAM postprocessor development with language support and remote Tcl debugging for CDL, TCL, and DEF files",
"version": "2026.9.220",
"version": "2026.9.300",
"publisher": "Christoph",
"icon": "images/nx-1.png",
"activationEvents": [
+24 -2
View File
@@ -75,6 +75,10 @@ from tools.semantic_tokens import (
_Highlighter,
)
from tools.signature_help import build_signature_help
from tools.tcloo_arguments import method_signature_help
from tclint.lexer import TclSyntaxError
from tools.tcloo_completion import parse_completion_source, tcloo_completions
from tools.tcloo_symbols import class_completion_items
from tools.tcl_command_completion import (
TCL_COMMAND_ITEMS,
TCL_COMMAND_NAMES,
@@ -278,6 +282,9 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
position = params.position
source_lines = LSP_SERVER.get_lines(doc)
oo_items = tcloo_completions(source_lines, position)
if oo_items is not None:
return lsp.CompletionList(is_incomplete=False, items=oo_items)
array_items = array_element_completions(
source_lines, position, LSP_SERVER.navigation_snapshot().values(),
str(pathlib.Path(uris.to_fs_path(doc.uri))),
@@ -334,7 +341,12 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
):
return lsp.CompletionList(is_incomplete=False, items=[])
tree = LSP_SERVER.get_tree(doc)
try:
tree = LSP_SERVER.get_tree(doc)
except TclSyntaxError:
tree = parse_completion_source(doc.source)
if tree is None:
return lsp.CompletionList(is_incomplete=False, items=[])
globals_set, procs_locals, proc_ranges = LSP_SERVER.variable_index_for_document(
doc, tree
)
@@ -347,6 +359,7 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
break
candidates: list[tuple[int, lsp.CompletionItem]] = []
candidates.extend((0, item) for item in class_completion_items(tree))
for name in sorted(local_names - globals_set):
candidates.append(
(
@@ -457,7 +470,15 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
)
def signature_help(params: lsp.SignatureHelpParams) -> lsp.SignatureHelp | None:
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
tree = LSP_SERVER.get_tree(document)
method_help = method_signature_help(document.source, params.position)
if method_help is not None:
return method_help
try:
tree = LSP_SERVER.get_tree(document)
except TclSyntaxError:
tree = parse_completion_source(document.source)
if tree is None:
return None
custom_signatures, custom_docs = LSP_SERVER.proc_metadata_snapshot(
document.path
@@ -541,6 +562,7 @@ def semantic_tokens(params: lsp.SemanticTokensParams):
# Reuse cached AST
tree = LSP_SERVER.get_tree(document)
hl.highlight_classes(tree)
tree.accept(hl, recurse=True)
tokens = hl.tokens()
+3
View File
@@ -14,6 +14,7 @@ from tclint.lexer import TclSyntaxError
from tclint.violations import Violation
from tools import checks, parser
from tools.completion_items import CompletionCollector
from tools.tcloo_symbols import class_completion_items
from tools.formatter import NxFormatter as Formatter
from tools.inlay_hint import InlayHintSignature, build_custom_inlay_signatures
from tools.navigation import FileSymbolIndex, build_file_symbol_index
@@ -190,6 +191,7 @@ class TclLanguageServer(LanguageServer):
item.label
for path_items in self.poco_completion.values()
for item in path_items
if item.kind != lsp.CompletionItemKind.Class
)
self._custom_function_names_cache = (self._index_generation, names)
return names
@@ -542,6 +544,7 @@ class TclLanguageServer(LanguageServer):
else self.parse_source(document.source)
)
tree.accept(collector, recurse=True)
collector.custom_functions.extend(class_completion_items(tree))
docs = build_proc_docs(tree, document.source)
navigation_index = build_file_symbol_index(
filepath, document.uri, tree
+1
View File
@@ -33,6 +33,7 @@ VARIABLE_KINDS = {
lsp.CompletionItemKind.Constant,
}
COMMAND_KINDS = {
lsp.CompletionItemKind.Class,
lsp.CompletionItemKind.Function,
lsp.CompletionItemKind.Method,
lsp.CompletionItemKind.Constructor,
+18 -1
View File
@@ -9,6 +9,8 @@ from typing import Any
import lsprotocol.types as lsp
from tclint.syntax_tree import Command, VarSub, Visitor
from tools.navigation import FileSymbolIndex
from tools.tcloo_arguments import method_parameters
from tools.tcloo_completion import resolved_method_calls
@dataclass(frozen=True)
@@ -160,6 +162,7 @@ class InlayHintGenerator(Visitor):
self.source_lines = (
source_lines if source_lines is not None else source.splitlines()
)
self.source = source
self.proc_signatures = proc_signatures
self.requested_range = requested_range
self.parameter_names = parameter_names
@@ -194,6 +197,17 @@ class InlayHintGenerator(Visitor):
walk(child)
walk(tree)
if self.parameter_names != "none":
for call in resolved_method_calls(self.source):
if not self._node_intersects_requested_range(call.command):
continue
parameters = method_parameters(call.parameters)
signature = InlayHintSignature(
parameters=tuple(InlayHintParameter(p.name, variadic=p.variadic) for p in parameters),
display_label=" ".join([call.label, *(p.label for p in parameters)]),
)
self._argument_hints(signature, call.command.args[call.argument_offset:])
self.hints.sort(key=lambda hint: (hint.position.line, hint.position.character))
return self.hints
def _position(self, line: int, column: int) -> lsp.Position:
@@ -238,7 +252,10 @@ class InlayHintGenerator(Visitor):
if signature is None or self.parameter_names == "none":
return
for argument_index, argument in enumerate(command.args):
self._argument_hints(signature, command.args)
def _argument_hints(self, signature, arguments):
for argument_index, argument in enumerate(arguments):
parameter = self._parameter_for_argument(signature, argument_index)
if parameter is None:
break
+16 -1
View File
@@ -6,6 +6,7 @@ from common.load_data import standard_items
from tclint.commands.plugins import PluginManager
from tclint.syntax_tree import BareWord, Command, QuotedWord, Visitor
from tools.variable_names import variable_name
from tools.tcloo_symbols import class_symbols
# Constructing a PluginManager scans entry points, and get_commands() rebuilds
# the builtin command set on every call. Semantic tokens are requested often, so
@@ -70,6 +71,7 @@ class _Highlighter(Visitor):
def __init__(self, plugins, custom_functions):
self._commands = _load_commands(plugins)
self._tokens = []
self._class_tokens = {}
if isinstance(custom_functions, dict):
self._custom_function_names = frozenset(
item.label
@@ -84,6 +86,17 @@ class _Highlighter(Visitor):
return
self._tokens.append((position, length, tok_type, modifiers or []))
def highlight_classes(self, tree):
declarations, references = class_symbols(tree)
for node, modifiers in [
*((node, [TokenModifier.declaration]) for node in declarations.values()),
*((node, []) for node in references),
]:
line, col = node.contents_pos
self._class_tokens[(line - 1, col - 1)] = (
(line - 1, col - 1), len(node.contents), "class", modifiers,
)
def _get_token_info(self, node):
"""Hilfsmethode um Token-Informationen aus verschiedenen Node-Typen zu extrahieren."""
if not hasattr(node, "pos"):
@@ -204,7 +217,9 @@ class _Highlighter(Visitor):
tokens = []
last_line = 0
last_col = 0
for (line, col), length, tok_type, tok_modifier in sorted(self._tokens, key=lambda x: x[0]):
raw_tokens = [token for token in self._tokens if token[0] not in self._class_tokens]
raw_tokens.extend(self._class_tokens.values())
for (line, col), length, tok_type, tok_modifier in sorted(raw_tokens, key=lambda x: x[0]):
line_delta = line - last_line
col_delta = col
if line == last_line:
+79
View File
@@ -0,0 +1,79 @@
"""Parameter presentation for statically resolved TclOO calls."""
from dataclasses import dataclass
import lsprotocol.types as lsp
from tclint.lexer import TclSyntaxError
from tclint.syntax_tree import BracedWord, Command
from tools.parser import CustomParser
from tools.signature_help import _active_argument, _contains_cursor
from tools.tcloo_completion import resolved_method_calls
@dataclass(frozen=True)
class MethodParameter:
name: str
label: str
variadic: bool = False
def method_parameters(parameters: str) -> list[MethodParameter]:
parser = CustomParser()
try:
words = parser.parse_list(BracedWord(parameters, pos=(1, 1))).children
result = []
for index, word in enumerate(words):
parts = parser.parse_list(word).children
if not parts or len(parts) > 2:
return []
name = parts[0].contents
if name is None:
return []
variadic = name == "args" and len(parts) == 1 and index == len(words) - 1
label = "{" + word.contents + "}" if len(parts) == 2 else name
result.append(MethodParameter(name, label, variadic))
return result
except TclSyntaxError:
return []
def method_signature_help(source: str, position: lsp.Position) -> lsp.SignatureHelp | None:
lines = source.split("\n")
if position.line >= len(lines):
return None
# AST columns are codepoints; LSP columns are UTF-16 code units.
prefix = lines[position.line].encode("utf-16-le")[:position.character * 2].decode("utf-16-le", errors="ignore")
cursor = (position.line, len(prefix))
candidates = [call for call in resolved_method_calls(source)
if _contains_cursor(call.command, lines, cursor)]
if not candidates:
return None
call = max(candidates, key=lambda candidate: candidate.command.pos)
def nested_active(node):
return any(
isinstance(child, Command) and _contains_cursor(child, lines, cursor)
or nested_active(child)
for child in node.children
)
# Let the inner command's own signature provider handle its arguments.
if nested_active(call.command):
return None
argument = _active_argument(call.command, cursor) - call.argument_offset
if argument < 0:
return None
parameters = method_parameters(call.parameters)
label = call.label
infos = []
for parameter in parameters:
label += " "
start = len(label.encode("utf-16-le")) // 2
label += parameter.label
infos.append(lsp.ParameterInformation(label=(start, len(label.encode("utf-16-le")) // 2)))
active = min(argument, len(parameters) - 1) if parameters else None
return lsp.SignatureHelp(
signatures=[lsp.SignatureInformation(label=label, parameters=infos, active_parameter=active)],
active_signature=0, active_parameter=active,
)
+226
View File
@@ -0,0 +1,226 @@
"""Conservative, document-local TclOO type inference without executing Tcl."""
from collections.abc import Sequence
from dataclasses import dataclass, field
import re
import lsprotocol.types as lsp
from tclint.lexer import TclSyntaxError
from tclint.syntax_tree import BracedWord, Command, CommandSub, Script, VarSub
from tools.parser import CustomParser
from tools.tcl_command_completion import line_prefix_at_position
@dataclass
class ClassInfo:
methods: dict[str, tuple[str, Script | None]] = field(default_factory=dict)
namespace: str = ""
constructor: str = ""
@dataclass
class MethodCall:
command: Command
label: str
parameters: str
argument_offset: int = 1
def parse_completion_source(source, pos=None):
# Complete open delimiters while editing; never evaluate the user's code.
for _ in range(16):
try:
return CustomParser().parse(source, pos=pos)
except TclSyntaxError as error:
message = str(error)
closing = next((char for text, char in (
("end of command substitution", "]"),
("match for brace", "}"), ("match for quote", '"'),
) if text in message), None)
if closing is None:
return None
source += closing
return None
def _body(node):
if isinstance(node, Script):
return node
if isinstance(node, BracedWord):
return parse_completion_source(node.contents, node.contents_pos)
return None
def _qualified(name, namespace):
return name if name.startswith("::") else f"{namespace}::{name}"
def tcloo_completions(
source_lines: Sequence[str], position: lsp.Position,
) -> list[lsp.CompletionItem] | None:
"""Return receiver-specific methods, or None outside a known OO context."""
prefix = line_prefix_at_position(source_lines, position)
if prefix is None:
return None
match = re.search(r"[\w:]*$", prefix)
typed = match.group()
# Only a method word, never a variable substitution or method argument.
word_start = len(prefix) - len(typed)
if word_start == 0 or prefix[word_start - 1] not in " \t":
return None
marker = "__nx_tcloo_completion_cursor__"
lines = list(source_lines)
suffix = lines[position.line][len(prefix):]
remaining = re.match(r"[\w:]*", suffix).group()
lines[position.line] = prefix + marker + suffix[len(remaining):]
tree = parse_completion_source("\n".join(lines))
if tree is None:
return None
classes, result, _ = _analyze(tree, typed, marker)
if result is None:
return None
cls, internal = result
methods = classes[cls].methods if cls else {"new": ("args", None), "create": ("name args", None)}
methods = dict(methods)
if cls:
methods.setdefault("destroy", ("", None))
suffix = source_lines[position.line][len(prefix):]
remaining = re.match(r"[\w:]*", suffix).group()
start = position.character - len(typed.encode("utf-16-le")) // 2
end = position.character + len(remaining.encode("utf-16-le")) // 2
return [lsp.CompletionItem(
label=name, kind=lsp.CompletionItemKind.Method,
detail=f"{cls or 'class'} {name} {signature}".rstrip(),
text_edit=lsp.TextEdit(range=lsp.Range(
start=lsp.Position(line=position.line, character=start),
end=lsp.Position(line=position.line, character=end)), new_text=name),
) for name, (signature, _) in sorted(methods.items())
if name.startswith(typed) and (internal or not name.startswith("_") and not name[:1].isupper())]
def _analyze(tree, typed="", marker=""):
classes = {}
contexts = []
calls = []
def collect(script, namespace=""):
if script is None:
return
for cmd in script.children:
if not isinstance(cmd, Command):
continue
args = cmd.args
routine = (cmd.routine.contents or "").removeprefix("::")
if routine == "namespace" and len(args) == 3 and args[0].contents == "eval" and args[1].contents:
collect(_body(args[2]), _qualified(args[1].contents, namespace))
elif routine == "oo::class" and len(args) == 3 and args[0].contents == "create" and args[1].contents:
name = _qualified(args[1].contents, namespace)
info = classes.setdefault(name, ClassInfo(namespace=namespace))
body = _body(args[2])
if body is None:
continue
for method in body.children:
if not isinstance(method, Command):
continue
ma = method.args
if method.routine.contents == "method" and len(ma) == 3 and ma[0].contents:
method_body = _body(ma[2])
info.methods[ma[0].contents] = (ma[1].contents or "", method_body)
contexts.append((method_body, namespace, name))
elif method.routine.contents in {"constructor", "destructor"} and ma:
if method.routine.contents == "constructor" and len(ma) == 2:
info.constructor = ma[0].contents or ""
contexts.append((_body(ma[-1]), namespace, name))
collect(tree)
result = None
def receiver(node, env, objects, namespace, owner, depth=0):
if depth > 12:
return None
if isinstance(node, VarSub):
return env.get(node.value)
if isinstance(node, CommandSub) and len(node.children) == 1:
return returned(node.children[0], env, objects, namespace, owner, depth + 1)
name = node.contents
return objects.get(_qualified(name, namespace)) if name else None
def returned(cmd, env, objects, namespace, owner, depth=0):
if not isinstance(cmd, Command) or depth > 12:
return None
args = cmd.args
name = cmd.routine.contents
if name == "self" and not args:
return owner
qualified = _qualified(name, namespace) if name else None
if qualified in classes and args and args[0].contents in {"new", "create"}:
return qualified
cls = owner if name == "my" else receiver(cmd.routine, env, objects, namespace, owner, depth + 1)
if cls not in classes or not args:
return None
method = classes[cls].methods.get(args[0].contents)
if method is None or method[1] is None:
return None
# Only infer unconditional final returns; conditional results stay unknown.
commands = [c for c in method[1].children if isinstance(c, Command)]
if commands and commands[-1].routine.contents == "return" and len(commands[-1].args) == 1:
return receiver(commands[-1].args[0], {}, objects, classes[cls].namespace, cls, depth + 1)
return None
def walk(script, env, objects, namespace="", owner=None):
nonlocal result
if script is None:
return
for cmd in script.children:
if not isinstance(cmd, Command):
continue
args = cmd.args
name = cmd.routine.contents
cls = owner if name == "my" else receiver(cmd.routine, env, objects, namespace, owner)
method_name = args[0].contents if args else None
if cls in classes and method_name in classes[cls].methods:
calls.append(MethodCall(cmd, f"{cls} {method_name}", classes[cls].methods[method_name][0]))
elif name and _qualified(name, namespace) in classes and method_name in {"new", "create"}:
cls = _qualified(name, namespace)
parameters = classes[cls].constructor
if method_name == "create":
parameters = "objectName " + parameters
calls.append(MethodCall(cmd, f"{cls} {method_name}", parameters))
if marker and args and args[0].contents == typed + marker:
cls = owner if name == "my" else receiver(cmd.routine, env, objects, namespace, owner)
if cls in classes:
result = (cls, name == "my")
elif name and _qualified(name, namespace) in classes:
result = (None, False)
return
# Command substitutions can contain the completion receiver.
for node in cmd.children:
if isinstance(node, CommandSub):
walk(node, env, objects, namespace, owner)
if name == "set" and len(args) == 2 and args[0].contents:
env[args[0].contents] = receiver(args[1], env, objects, namespace, owner)
elif name == "unset":
for arg in args:
env.pop(arg.contents, None)
elif name == "proc" and len(args) == 3:
walk(_body(args[2]), {}, objects.copy(), namespace)
elif name == "namespace" and len(args) == 3 and args[0].contents == "eval" and args[1].contents:
walk(_body(args[2]), {}, objects, _qualified(args[1].contents, namespace))
elif name and _qualified(name, namespace) in classes and len(args) >= 2 and args[0].contents == "create" and args[1].contents:
objects[_qualified(args[1].contents, namespace)] = _qualified(name, namespace)
else:
for arg in args:
if isinstance(arg, Script):
# Branch-local facts are not propagated beyond the branch.
walk(arg, env.copy(), objects.copy(), namespace, owner)
walk(tree, {}, {})
for body, namespace, owner in contexts:
walk(body, {}, {}, namespace, owner)
return classes, result, calls
def resolved_method_calls(source):
tree = parse_completion_source(source)
return _analyze(tree)[2] if tree is not None else []
+53
View File
@@ -0,0 +1,53 @@
"""Class declarations and references shared by completion and highlighting."""
import lsprotocol.types as lsp
from tclint.syntax_tree import Command
from tools.tcloo_completion import _body, _qualified
def class_symbols(tree):
"""Return qualified class declarations and statically resolved name nodes."""
declarations = {}
commands = []
def walk(node, namespace="", in_class=False):
if node is None:
return
if isinstance(node, Command):
args = node.args
name = (node.routine.contents or "").removeprefix("::")
commands.append((node, namespace))
if (name == "namespace" and len(args) == 3
and args[0].contents == "eval" and args[1].contents):
walk(_body(args[2]), _qualified(args[1].contents, namespace), in_class)
return
if (name == "oo::class" and len(args) == 3
and args[0].contents == "create" and args[1].contents):
declarations[_qualified(args[1].contents, namespace)] = args[1]
walk(_body(args[2]), namespace, True)
return
if in_class and name in {"method", "constructor", "destructor"} and args:
walk(_body(args[-1]), namespace, True)
return
for child in node.children:
walk(child, namespace, in_class)
walk(tree)
references = []
for command, namespace in commands:
name = command.routine.contents
if name and any(candidate in declarations for candidate in (
_qualified(name, namespace), _qualified(name, ""),
)):
references.append(command.routine)
return declarations, references
def class_completion_items(tree):
declarations, _ = class_symbols(tree)
return [lsp.CompletionItem(
label=name.removeprefix("::"),
kind=lsp.CompletionItemKind.Class,
detail=f"TclOO class {name}",
) for name in sorted(declarations)]
@@ -0,0 +1,100 @@
import lsprotocol.types as lsp
import pytest
from tools.inlay_hint import InlayHintGenerator
from tools.parser import CustomParser
from tools.tcloo_arguments import method_parameters, method_signature_help
CLASS = """oo::class create MCS {
constructor {name {size 3}} {}
method initValue {i value} {return [self]}
method format {value {precision 7}} {}
method many {first args} {}
method empty {} {}
method internal {} {my initValue 0 10}
}
"""
def signature(source):
prefix, suffix = source.split("|")
position = lsp.Position(line=prefix.count("\n"), character=len(prefix.rsplit("\n", 1)[-1].encode("utf-16-le")) // 2)
return method_signature_help(prefix + suffix, position)
@pytest.mark.parametrize("tail, label, active", [
("set mcs [MCS new test]\n$mcs initValue |", "::MCS initValue i value", 0),
("set mcs [MCS new test]\n$mcs initValue 0 |", "::MCS initValue i value", 1),
("set mcs [MCS new test]\n$mcs format 12 |", "::MCS format value {precision 7}", 1),
("set mcs [MCS new test]\n$mcs many 1 2 3 |", "::MCS many first args", 1),
("set mcs [MCS new test]\n$mcs empty |", "::MCS empty", None),
("MCS create instance test\ninstance initValue 0 |", "::MCS initValue i value", 1),
("set mcs [[MCS new test] initValue 0 0]\n$mcs initValue |", "::MCS initValue i value", 0),
("set mcs [MCS new test]\nputs [$mcs initValue 0 |", "::MCS initValue i value", 1),
("set mcs [MCS new |", "::MCS new name {size 3}", 0),
("MCS create instance |", "::MCS create objectName name {size 3}", 1),
("set mcs [MCS new test]\nputs 😀; $mcs initValue 0 |", "::MCS initValue i value", 1),
])
def test_method_signatures(tail, label, active):
result = signature(CLASS + tail)
assert result is not None
assert result.signatures[0].label == label
assert result.active_parameter == active
def test_my_signature_and_parameter_spans():
result = signature(CLASS.replace("my initValue 0 10", "my initValue 0 |"))
assert result.active_parameter == 1
info = result.signatures[0]
assert [info.label[start:end] for start, end in (p.label for p in info.parameters)] == ["i", "value"]
@pytest.mark.parametrize("tail", [
"$unknown initValue |",
"set mcs [MCS new test]\nset mcs text\n$mcs initValue |",
"set mcs [MCS new test]\n$mcs initValue [unknown |] 3",
"set mcs [MCS new test]\n$mcs initValue|",
])
def test_unknown_receivers_and_nested_commands(tail):
assert signature(CLASS + tail) is None
def test_optional_parameter_with_spaced_default():
parameters = method_parameters('value {description {hello world}} args')
assert [p.name for p in parameters] == ["value", "description", "args"]
assert parameters[1].label == "{description {hello world}}"
assert parameters[-1].variadic
def test_method_inlay_hints_and_existing_preferences():
source = CLASS + "set mcs [MCS new test]\n$mcs initValue 0 10\n$mcs many 1 2 3\n$mcs initValue $i $other"
tree = CustomParser().parse(source)
start = len(CLASS.splitlines()) + 1
requested = lsp.Range(start=lsp.Position(line=start, character=0), end=lsp.Position(line=start + 2, character=100))
generator = InlayHintGenerator(source, {}, requested_range=requested)
hints = generator.generate(tree)
assert [hint.label[0].value for hint in hints] == ["i:", "value:", "first:", "args:", "args:", "value:"]
assert [source.splitlines()[hint.position.line][hint.position.character:] for hint in hints[:2]] == ["0 10", "10"]
assert "::MCS initValue i value" in hints[0].tooltip.value
generator = InlayHintGenerator(source, {}, requested_range=requested, parameter_names="literals")
assert len(generator.generate(tree)) == 5
generator = InlayHintGenerator(source, {}, parameter_names="none")
assert generator.generate(tree) == []
def test_lsp_signature_help_with_unfinished_bracket(tmp_path, monkeypatch):
import lsp_server
from test_completion_context import _completion_server
server, document, _ = _completion_server(tmp_path, monkeypatch)
source = CLASS + "set mcs [MCS new test]\nputs [$mcs initValue 0 "
document = server.workspace.get_text_document(document.uri)
document._source = source
document.version = 2
result = lsp_server.signature_help(lsp.SignatureHelpParams(
text_document=lsp.TextDocumentIdentifier(uri=document.uri),
position=lsp.Position(line=len(source.splitlines()) - 1, character=len(source.splitlines()[-1])),
))
assert result.active_parameter == 1
assert result.signatures[0].label == "::MCS initValue i value"
@@ -0,0 +1,166 @@
import lsprotocol.types as lsp
import pytest
from tools.tcloo_completion import tcloo_completions
CLASS = """oo::class create MCS {
constructor {} {my initValue 0 0}
method initValue {i value} {return [self]}
method initOrg {x y z} {return [my initValue 0 $x]}
method toLst {} {return {1 2 3}}
method _private {} {return [self]}
}
"""
def complete(source):
offset = source.index("|")
before = source[:offset]
position = lsp.Position(line=before.count("\n"), character=len(before.rsplit("\n", 1)[-1].encode("utf-16-le")) // 2)
return tcloo_completions(source.replace("|", "").splitlines(), position)
@pytest.mark.parametrize("code", [
"set mcs [MCS new]\n$mcs |",
"set mcs [::MCS new]\n$mcs |",
"MCS create instance\ninstance |",
"set mcs [MCS create instance]\n$mcs |",
"set mcs [[MCS new] initValue 0 0]\n$mcs |",
"set mcs [[MCS new] initOrg 1 2 3]\n$mcs |",
"set a [MCS new]\nset mcs $a\n$mcs |",
"proc run {} {set mcs [MCS new]; $mcs |}",
"set mcs [MCS new]\nputs [$mcs |]",
"set mcs [MCS new]\nputs [$mcs |",
"[MCS new] |",
])
def test_instances_and_chains(code):
items = complete(CLASS + code)
assert {item.label for item in items} == {"initValue", "initOrg", "toLst", "destroy"}
assert next(item for item in items if item.label == "initValue").detail == "::MCS initValue i value"
def test_my_and_constructor():
for source in [CLASS.replace("my initValue 0 0", "my |"), CLASS.replace("return {1 2 3}", "my |")]:
assert "_private" in {item.label for item in complete(source)}
@pytest.mark.parametrize("code", [
"set mcs [MCS new]\nset mcs text\n$mcs |",
"set mcs [MCS new]\nunset mcs\n$mcs |",
"proc a {} {set mcs [MCS new]}\nproc b {} {$mcs |}",
"set mcs [MCS new]\nproc b {mcs} {$mcs |}",
"set mcs [[MCS new] toLst]\n$mcs |",
"$mcs |\nset mcs [MCS new]",
"set mcs [MCS new]\n$mcs initValue |",
"# my |",
"puts {my |}",
])
def test_unknown_and_non_command_contexts(code):
assert complete(CLASS + code) is None
def test_prefix_and_replacement():
items = complete(CLASS + "set mcs [MCS new]\n$mcs initV|alue")
assert [item.label for item in items] == ["initValue"]
assert items[0].text_edit.new_text == "initValue"
assert items[0].text_edit.range.start.character == 5
assert items[0].text_edit.range.end.character == 14
def test_namespace():
source = "namespace eval geometry {\n" + CLASS + "set mcs [MCS new]\n$mcs |\n}"
assert "initOrg" in {item.label for item in complete(source)}
def test_lsp_space_trigger(tmp_path, monkeypatch):
import lsp_server
from test_completion_context import _completion_server
server, document, _ = _completion_server(tmp_path, monkeypatch)
source = CLASS + "set mcs [MCS new]\n$mcs "
document = server.workspace.get_text_document(document.uri)
document._source = source
document.version = 2
items = lsp_server.on_completion(lsp.CompletionParams(
text_document=lsp.TextDocumentIdentifier(uri=document.uri),
position=lsp.Position(line=len(source.splitlines()) - 1, character=5),
context=lsp.CompletionContext(trigger_kind=lsp.CompletionTriggerKind.TriggerCharacter, trigger_character=" "),
)).items
assert "initOrg" in {item.label for item in items}
@pytest.mark.parametrize("tail", ["set mcs [MC", "set mcs [MC]", "MC"])
def test_class_name_completion_while_editing(tmp_path, monkeypatch, tail):
import lsp_server
from test_completion_context import _completion_server
server, document, _ = _completion_server(tmp_path, monkeypatch)
source = CLASS + tail
document = server.workspace.get_text_document(document.uri)
document._source = source
document.version = 2
items = lsp_server.on_completion(lsp.CompletionParams(
text_document=lsp.TextDocumentIdentifier(uri=document.uri),
position=lsp.Position(line=len(source.splitlines()) - 1,
character=len(tail.rstrip("]"))),
)).items
classes = [item for item in items if item.label == "MCS"]
assert len(classes) == 1
assert classes[0].kind == lsp.CompletionItemKind.Class
def test_classes_are_indexed_with_their_own_kind(tmp_path, monkeypatch):
from test_completion_context import _completion_server, _document
server, _, _ = _completion_server(tmp_path, monkeypatch)
assert server.update_poco_completion_for_file(_document(tmp_path / "class.tcl", CLASS))
items = server.completion_items_snapshot()
assert any(item.label == "MCS" and item.kind == lsp.CompletionItemKind.Class for item in items)
assert "MCS" not in server.custom_function_names_snapshot()
def test_class_command_completions():
assert {item.label for item in complete(CLASS + "set mcs [MCS |]")} == {"new", "create"}
def test_semantic_class_declarations_and_uses():
from tools.parser import CustomParser
from tools.semantic_tokens import _Highlighter, TokenModifier
source = CLASS + 'set mcs [MCS new]\nset second [::MCS new]\nputs "MCS"\n'
tree = CustomParser().parse(source)
highlighter = _Highlighter([], {"MCS"})
highlighter.highlight_classes(tree)
tree.accept(highlighter, recurse=True)
line = col = 0
classified = []
for token in highlighter.tokens():
col = col + token.offset if token.line == 0 else token.offset
line += token.line
text = source.splitlines()[line][col:col + token.length]
classified.append((line, col, text, token.tok_type, token.tok_modifiers))
classes = [entry for entry in classified if entry[3] == "class"]
assert [entry[2] for entry in classes] == ["MCS", "MCS", "::MCS"]
assert TokenModifier.declaration in classes[0][4]
assert not any(entry[2] in {"MCS", "::MCS"} and entry[3] == "function" for entry in classified)
assert len({entry[:2] for entry in classified}) == len(classified)
def test_namespaced_class_symbols_and_method_body_references():
from tools.parser import CustomParser
from tools.tcloo_symbols import class_completion_items, class_symbols
tree = CustomParser().parse('''namespace eval geometry {
oo::class create MCS {
method duplicate {} {return [MCS new]}
}
set mcs [MCS new]
}
set mcs [geometry::MCS new]
set mcs [::geometry::MCS new]
''')
declarations, references = class_symbols(tree)
assert set(declarations) == {"::geometry::MCS"}
assert [node.contents for node in references] == ["MCS", "MCS", "geometry::MCS", "::geometry::MCS"]
assert class_completion_items(tree)[0].label == "geometry::MCS"
+318 -38
View File
@@ -6,24 +6,24 @@ set te875st 11111
#set ::custom_flag(from_move,$::mom_path_name) 1
if {$main == 1 && 1 == 1} {
puts "main"
puts "main"
}
proc test {} {
puts "main"
proc llll {} {}
set rrrrrrr
puts "main"
proc llll {} {}
set rrrrrrr
}
LIB_GE_command_buffer_edit_insert MOM_tool_change_LIB TOOL_CHANGE_AUTO {CUSTOM_after_tool_change_call} mytag after @TOOL_CHANGE_AUTO
MOM_abort
namespace eval myns {
proc add {a b} {
set sum [expr {$a + $b}]
return $sum
}
set config "debug"
proc add {a b} {
set sum [expr {$a + $b}]
return $sum
}
set config "debug"
}
#_________________________________________________________________________________________________
@@ -31,7 +31,7 @@ namespace eval myns {
# Function to output a spacer line or empty line
#_________________________________________________________________________________________________
proc SERVICE_spacer_output {type {length 20} {line_num 0} {output 1}} {
LIB_GE_message [string repeat $type $length] "output_$output" $line_num
LIB_GE_message [string repeat $type $length] "output_$output" $line_num
}
@@ -42,8 +42,8 @@ SERVICE_spacer_output "*" 2 0 0
# Function to delete the file
#_________________________________________________________________________________________________
proc SERVICE_remove_file {file} {
if {![SERVICE_check_file_exists $file]} {return}
MOM_remove_file $file
if {![SERVICE_check_file_exists $file]} {return}
MOM_remove_file $file
}
#_________________________________________________________________________________________________
@@ -51,8 +51,8 @@ proc SERVICE_remove_file {file} {
# Function to check if the file exists
#_________________________________________________________________________________________________
proc SERVICE_check_file_exists {file} {
if {[file exists $file]} {return 1}
return 0
if {[file exists $file]} {return 1}
return 0
}
#_________________________________________________________________________________________________
@@ -60,19 +60,19 @@ proc SERVICE_check_file_exists {file} {
# Ask UDE Info for the Tool
#_________________________________________________________________________________________________
proc SERVICE_ask_ude_tool {pos ude_name tool_name} {
MOM_ask_ude_info $tool_name "tool" $pos
MOM_ask_ude_info $tool_name "tool" $pos
if {[lsearch $::mom_result $ude_name] != -1} {
return 1
}
return 0
if {[lsearch $::mom_result $ude_name] != -1} {
return 1
}
return 0
}
proc MOM_dummy_event_start {} {
global mom_new_item
global mom_new_item_end
global mom_new_item
global mom_new_item_end
#Put your UDE Handler Tcl here
#Put your UDE Handler Tcl here
}
#_________________________________________________________________________________________________
@@ -80,12 +80,12 @@ proc MOM_dummy_event_start {} {
# Ask UDE Info for the Operation
#_________________________________________________________________________________________________
proc SERVICE_ask_ude_operation {pos ude_name path_name} {
MOM_ask_ude_info $path_name "operation" $pos
MOM_ask_ude_info $path_name "operation" $pos
if {[lsearch $::mom_result $ude_name] != -1} {
return 1
}
return 0
if {[lsearch $::mom_result $ude_name] != -1} {
return 1
}
return 0
}
#_________________________________________________________________________________________________
@@ -96,7 +96,7 @@ proc SERVICE_ask_ude_operation {pos ude_name path_name} {
# restore
#_________________________________________________________________________________________________
proc SERVICE_output_handling {handler} {
set ::lib_ge(hidden_output) $handler
set ::lib_ge(hidden_output) $handler
}
#_________________________________________________________________________________________________
@@ -105,21 +105,301 @@ proc SERVICE_output_handling {handler} {
# this function is called in start of program
#_________________________________________________________________________________________________
proc SERVICE_get_tool_data {} {
global mom_tool_data
global mom_operation_info
global mom_tool_data
global mom_operation_info
set mom_tool_data(toollist) ""
set operations $::mom_operation_name_list
foreach operation $operations {
if {[lsearch -exact $mom_tool_data(toollist) $mom_operation_info($operation,tool_name)] == -1} {
lappend mom_tool_data(toollist) $mom_operation_info($operation,tool_name)
}
}
set mom_tool_data(toollist) ""
set operations $::mom_operation_name_list
foreach operation $operations {
if {[lsearch -exact $mom_tool_data(toollist) $mom_operation_info($operation,tool_name)] == -1} {
lappend mom_tool_data(toollist) $mom_operation_info($operation,tool_name)
}
}
}
LIB_GE_command_buffer_edit_replace MOM_end_of_program_LIB END_OF_PROGRAM @END_OF_PROG {
MOM_do_template "end_of_program_rewind"
MOM_do_template "end_of_program_rewind"
} EndOfProgramRewind
SERVICE_remove_file "test"
oo::class create MCS {
variable values
constructor {args} {
for {set i 0} {$i <= 11} {incr i} {
my initValue $i 0.
}
}
destructor {
#puts "[self] is now deleted"
}
method fformat {value {precision 7}} {return [expr {round( 10.0 ** $precision * $value) / (10.0 ** $precision)}]}
method radtodeg {rad} {return [expr {$rad*180.0/(4.0*atan(1.0))}]}
method degtorad {deg} {return [expr {$deg*(4.0*atan(1.0))/180.0}]}
method duplicate {args} {return [[MCS new] initMCS [self]]}
method type {args} {return "::MCS"}
method toArray {args} {
for {set i 0} {$i < [array size values]} {incr i} {
append arr [string cat $i " "] ; append arr [string cat $values($i) " "]
}
return [string trimright $arr]
}
method toLst {args} {
for {set i 0} {$i < [array size values]} {incr i} {
lappend lst $values($i)
}
return $lst
}
method toStr {args} {
for {set i 0} {$i < [array size values]} {incr i} {
if {$i>0} {set str [string cat $str ", "]}
append str $values($i)
}
return $str
}
method toStrF {{precision 7}} {
for {set i 0} {$i < [array size values]} {incr i} {
if {$i>0} {set str [string cat $str ", "]}
append str [my fformat $values($i) 10]
}
return $str
}
method initValue {i value} {set values($i) [expr double($value)] ; return [self]}
method initArray {a} {upvar $a arr ; for {set i 0} {$i < [array size arr]} {incr i} {my initValue $i $arr($i)} ; return [self]}
method initArrayLst {arr_lst} {array set arr $arr_lst ; for {set i 0} {$i < [array size arr]} {incr i} {my initValue $i $arr($i)} ; return [self]}
method initLst {lst} {for {set i 0} {$i < [llength $lst]} {incr i} {my initValue $i [lindex $lst $i]} ; return [self]}
method initMCS {mcs} {
my identity
set p [$mcs org] ; my initOrgP $p ; $p destroy
set v [$mcs xVct] ; my initXvctV $v ; $v destroy
set v [$mcs yVct] ; my initYvctV $v ; $v destroy
set v [$mcs zVct] ; my initZvctV $v ; $v destroy
return [self]
}
method initMTX {mtx} {
set p [$mtx org] ; my initOrg $p ; $p destroy
set v [$mtx xVct] ; my initXvct $v ; $v destroy
set v [$mtx yVct] ; my initYvct $v ; $v destroy
set v [$mtx zVct] ; my initZvct $v ; $v destroy
return [self]
}
method initOrg {dx dy dz} {my initValue 0 $dx ; my initValue 1 $dy ; my initValue 2 $dz ; return [self]}
method initOrgP {p} {return [my initOrg [$p x] [$p y] [$p z]]}
method initXvct {dx dy dz} {my initValue 3 $dx ; my initValue 4 $dy ; my initValue 5 $dz ; return [self]}
method initXvctV {v} {return [my initXvct [$v x] [$v y] [$v z]]}
method initYvct {dx dy dz} {my initValue 6 $dx ; my initValue 7 $dy ; my initValue 8 $dz ; return [self]}
method initYvctV {v} {return [my initYvct [$v x] [$v y] [$v z]]}
method initZvct {dx dy dz} {my initValue 9 $dx ; my initValue 10 $dy ; my initValue 11 $dz ; return [self]}
method initZvctV {v} {return [my initZvct [$v x] [$v y] [$v z]]}
method initOrgArray {a} {upvar $a arr ; return [my initOrg $arr(0) $arr(1) $arr(2)]}
method initVctsArray {a} {
upvar $a arr
my initXvct $arr(0) $arr(1) $arr(2)
my initYvct $arr(3) $arr(4) $arr(5)
my initZvct $arr(6) $arr(7) $arr(8)
return [self]
}
method value {i} {return $values($i)}
method org {args} {return [Point3D new $values(0) $values(1) $values(2)]}
method xVct {args} {return [Vector3D new $values(3) $values(4) $values(5)]}
method yVct {args} {return [Vector3D new $values(6) $values(7) $values(8)]}
method zVct {args} {return [Vector3D new $values(9) $values(10) $values(11)]}
method same {mcs {precision 7}} {
set org [[self] org] ; set org_1 [$mcs org]
set vx [[self] xVct] ; set vx_1 [$mcs xVct]
set vy [[self] yVct] ; set vy_1 [$mcs yVct]
set vz [[self] zVct] ; set vz_1 [$mcs zVct]
if {[$org same $org_1 $precision] && [$vx same $vx_1 $precision] && [$vy same $vy_1 $precision] && [$vz same $vz_1 $precision]} {
set value 1
} else {
set value 0
}
$org destroy ; $org_1 destroy
$vx destroy ; $vx_1 destroy
$vy destroy ; $vy_1 destroy
$vz destroy ; $vz_1 destroy
return $value
}
}
set point [Point3D new]
$point add 1 2 3
$custom_flag(from_move,$::mom_path_name)
oo::class create Point3D {
variable x 0. ; variable y 0. ; variable z 0.
constructor {{dx 0.} {dy 0.} {dz 0.}} {my initX $dx ; my initY $dy ; my initZ $dz}
destructor {
#puts "[self] is now deleted"
}
method fformat {value {precision 7}} {return [expr {round( 10.0 ** $precision * $value) / (10.0 ** $precision)}]}
method radtodeg {rad} {return [expr {$rad*180.0/(4.0*atan(1.0))}]}
method degtorad {deg} {return [expr {$deg*(4.0*atan(1.0))/180.0}]}
method duplicate {args} {return [[Point3D new] initP [self]]}
method type {args} {return "::Point3D"}
method toArray {{i 0}} {return [list [incr i 0] $x [incr i] $y [incr i] $z]}
method toLst {args} {return [list $x $y $z]}
method toStr {args} {return "$x, $y, $z"}
method toStrF {{precision 7}} {return "[my fformat $x $precision], [my fformat $y $precision], [my fformat $z $precision]"}
method x {args} {return $x}
method y {args} {return $y}
method z {args} {return $z}
method initX {dx} {set x [expr double($dx)]}
method initY {dy} {set y [expr double($dy)]}
method initZ {dz} {set z [expr double($dz)]}
method init {dx dy dz} {my initX $dx ; my initY $dy ; my initZ $dz ; return [self]}
method initArray {a} {upvar $a arr ; return [my init $arr(0) $arr(1) $arr(2)]}
method initArrayLst {arr_lst} {array set arr $arr_lst ; return [my init $arr(0) $arr(1) $arr(2)]}
method initLst {lst} {return [my init [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
method initP {p} {return [my init [$p x] [$p y] [$p z]]}
method reset {args} {my init 0. 0. 0. ; return [self]}
method add {dx dy dz} {my init [expr {$x + $dx}] [expr {$y + $dy}] [expr {$z + $dz}] ; return [self]}
method addArray {a} {upvar $a arr ; return [my add $arr(0) $arr(1) $arr(2)]}
method addArrayLst {arr_lst} {array set arr $arr_lst ; return [my add $arr(0) $arr(1) $arr(2)]}
method addLst {lst} {return [my add [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
method addP {p} {return [my add [$p x] [$p y] [$p z]]}
method sub {dx dy dz} {my init [expr {$x - $dx}] [expr {$y - $dy}] [expr {$z - $dz}] ; return [self]}
method subArray {a} {upvar $a arr ; return [my sub $arr(0) $arr(1) $arr(2)]}
method subArrayLst {arr_lst} {array set arr $arr_lst ; return [my sub $arr(0) $arr(1) $arr(2)]}
method subLst {lst} {return [my sub [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
method subP {p} {return [my sub [$p x] [$p y] [$p z]]}
method dist {dx dy dz} {return [expr {sqrt([expr {$x - $dx}]**2 + [expr {$y - $dy}]**2 + [expr {$z - $dz}]**2)}]}
method distArray {a} {upvar $a arr ; return [my dist $arr(0) $arr(1) $arr(2)]}
method distArrayLst {arr_lst} {array set arr $arr_lst ; return [my dist $arr(0) $arr(1) $arr(2)]}
method distLst {lst} {return [my dist [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
method distP {p} {return [my dist [$p x] [$p y] [$p z]]}
method dist_to_line {pl1 pl2} {
set pl [[$pl2 duplicate] subP $pl1]
set pp [[$pl1 duplicate] subP [self]]
set vl [[Vector3D new] initV $pl]
set vp [[Vector3D new] initV $pp]
set l [$vl magnitude]
if {[my fformat $l 5]==0} {
set value 0.
} else {
set vc [$vl cross $vp]
set value [expr [$vc magnitude]/$l]
$vc destroy
}
$pl destroy ; $vl destroy
$pp destroy ; $vp destroy
return $value
}
method dist_to_lineV {v} {
set p1 [[Point3D new] init 0. 0. 0.]
set p2 [[Point3D new] initP $v]
set value [my dist_to_line $p1 $p2]
$p1 destroy
$p2 destroy
return $value
}
#midpoint method to calculate the midpoint between this point and another point. This can be done by averaging the x, y, and z coordinates of the two points.
method midpoint {dx dy dz} {return [Point3D new [expr {double([[self] x] + $dx)/2}] [expr {double([[self] y] + $dy)/2}] [expr {double([[self] z] + $dz)/2}]]}
method midpointArr {a} {upvar $a arr ; return [my midpoint $arr(0) $arr(1) $arr(2)]}
method midpointArrLst {arr} {array set arr $arr_lst ; return [my midpoint $arr(0) $arr(1) $arr(2)]}
method midpointLst {lst} {return [my midpoint [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
method midpointP {p} {return [my midpoint [$p x] [$p y] [$p z]]}
method same {p {precision 7}} {
if {[my fformat [expr {[[self] x] - [$p x]}] $precision] == 0. && [my fformat [expr {[[self] y] - [$p y]}] $precision] == 0. && [my fformat [expr {[[self] z] - [$p z]}] $precision] == 0.} {return 1} else {return 0}
}
#This method takes three arguments: a vector object "axis" as the axis of rotation, an angle of rotation, and a point object "center" as the center of rotation.
#First, it translates the point by subtracting the center of rotation,
#then it applies the Rodrigues' rotation formula to calculate the new coordinates of the point, and finally,
#it translates back the point by adding the center of rotation.
#The center parameter is defined as an optional parameter with a default value of a new Point3D object, initialized with the values (0,0,0).
method rotAround {u v w angle {x0 0.} {y0 0.} {z0 0.}} {
set x1 [expr {double($x) - $x0}] ; set y1 [expr {double($y) - $y0}] ; set z1 [expr {double($z) - $z0}]
set u [expr {double($u)}] ; set v [expr {double($v)}] ; set w [expr {double($w)}]
set a [my degtorad $angle]
set coss [expr {cos($a)}]
set sinn [expr {sin($a)}]
set x2 [expr {$u*($u*$x1 + $v*$y1 + $w*$z1)*(1 - $coss) + $x1*$coss + (-$w*$y1 + $v*$z1)*$sinn + $x0}]
set y2 [expr {$v*($u*$x1 + $v*$y1 + $w*$z1)*(1 - $coss) + $y1*$coss + ($w*$x1 - $u*$z1)*$sinn + $y0}]
set z2 [expr {$w*($u*$x1 + $v*$y1 + $w*$z1)*(1 - $coss) + $z1*$coss + (-$v*$x1 + $u*$y1)*$sinn + $z0}]
my init $x2 $y2 $z2
return [self]
}
method rotAroundLst {aLst angle {cLst {0. 0. 0.}}} {
return [my rotAround [lindex $aLst 0] [lindex $aLst 1] [lindex $aLst 2] $angle [lindex $cLst 0] [lindex $cLst 1] [lindex $cLst 2]]
}
method rotAroundVP {axis angle {center {}}} {
if {[catch {set s [$center type]}] || (![string equal [$center type] "::Point3D"] && ![string equal [$center type] "::Vector3D"])} {
set center [Point3D new]
set b_center true
}
my rotAround [$axis x] [$axis y] [$axis z] $angle [$center x] [$center y] [$center z]
if {[info exists b_center]} {$center destroy}
return [self]
}
method transform {mtx_from mtx_to} {
if {[catch {set s [$mtx_from type]}] || ![string equal [$mtx_from type] "::Matrix4x4"]} {
set mtx_from [[Matrix4x4 new] identity]
set b_mtx_from true
}
if {[catch {set s [$mtx_to type]}] || ![string equal [$mtx_to type] "::Matrix4x4"]} {
set mtx_to [[Matrix4x4 new] identity]
set b_mtx_to true
}
set m [[Matrix4x4 new] identity]
$m initValue 0 [my x]
$m initValue 4 [my y]
$m initValue 8 [my z]
$m initValue 12 1.
set trs [[[[$mtx_from duplicate] inverse] multiply $mtx_to false] inverse]
$m multiply $trs true
my init [$m value 0] [$m value 4] [$m value 8]
$m destroy
$trs destroy
if {[info exists b_mtx_from]} {$mtx_from destroy}
if {[info exists b_mtx_to]} {$mtx_to destroy}
return [self]
}
}