Jump to content
  • 4

integrate smartpaste in Vectorworks


matteoluigi

Question

Just a simple but not unambitious wish.

 

Vectorworks is a software which mainly base on handling objects which are organized in so called classes.

The organization of Vectorworks-files (not like in Revit) but like in many other CAD and CAAD softwares (Autocad, Archicad for example) does base on a similar system. in both called programs (despite of the fact that classes are called there "layers" but at the end it's the same thing) a very important part of a project's organization does base on classes.

 

Due to the fact, that a) sometimes we find other studios who also work with vectorworks, which files we try to integrate or copy parts of them b) we sometimes optimize are class structure but want to access older files which base on older class systems, a system to copy and paste and match old drawings, models in newer class systems would be a great time and quality saver.

 

Here comes @GioPet's smartpaste into play. Somehow an integration of smartpaste (or a redesign as VW Plugin) could be an idea. Maybe it would be ok having it as a Vectorscript plugin actually would be fine (because imho there are many other issues which are more important (wall improvements, void-objects for Vectorworks for example)

 

however, we miss smartpaste since VW 2023 (aparently there are some problems in integrating it in 2023 and 2024...)

 

However, I would love being able to reuse smartpaste in VW2023 and 2024 and 2025 as soon as it's released, as well.

 

@Dominique Corpataux what do you think? 😉 

 

regards Matteo

Link to comment

2 answers to this question

Recommended Posts

  • 0

Agreed, class organization is still a crucial aspect, often posing challenges for users. Not all offices and projects require the same workflows. In the realm of Big Projects, the class structure may deviate from the standard, evolving over several years, and copying from old drawings might not be a significant concern.

 

On the whole, it appears to be the right approach to employ a management tool that seamlessly connects various elements. This functionality seems to be already present in Vectorworks and is working quite effectively but always open for future improvements. We have the Ebenen/Klassen-Manager tool and/or the class/layer mapping feature. These tools allow users to define class mappings and save/load them for use in different projects. While mapping classes during object pasting might be functional, it does not align perfectly with the broader workflow requirements addressed by the aforementioned features, such as inserting resources and importing geometry.

Try thinking one step forward i can imagine, that something like a overall document style (Not just for classes, mybe also central saved values as project data etc. - i think styles are really a great working concept) could be worth thinking about in this direction.

image.png.2fbc63cce9afe293ccd8f06faee22d31.png
image.png.8d05cf92ba317d8bdbaefa0712db6d97.png


However:

Admittedly, I have not insight to "smart paste" feature in action, so my assumptions may be incorrect. It appears that this command executes a standard "paste" action initially, followed by a dialog that enables the user to modify the class of the pasted elements within a selected class, while also offering the option to delete newly created classes if undesired. Due to potential reliance on legacy commands or an inaccessible included file, the functionality may no longer be operational. Generally, scripts can be normally easily adapted for new versions, even when some functions are outdated. But the script does not seems to be available anymore?
 

I use a similar script for records, i invested some time to fit to the class requirements. The script saves the substitution dictionary on disk, a feature seemingly unsupported by the old function. This script appears to encompass approximately 30% of the original "smart paste" pascal-script and its associated included file. It suggests that a fundamental element may be missing or that the dialog code in my example is more concise due to the dynamic creation of the object without the use of dialog builder components.




The Dialog of my script:
image.thumb.png.67b1d2948fcb5d07ec92331056a5078b.png

image.png.866e1fa95578f28d03d107998b1b3445.png

'''
DomC 26.12.2023

Disclaimer:
This script comes with no warranty or official support services (excluding the Community Forum). Use at your own risk. It is recommended to test with a copy of your original documents.

Known Issues:
- Some content within symbols or PIOs may lead to unexpected results.
- Pasted resources, including colors, symbols, line types, dimension standards, etc., are not translated; only classes are affected.
- The script performs an all-encompassing paste action, resulting in a change to the pastes, still selected elements after pasting.
- Pasting inside annotations, Symbols, PIO Graphic Containers etc. may result in unexpected results.
- If a bug occours no pasted objects should be missed. Just maybe in wrong classes or maybe unwanted classes are deleted at the worst case.
- This script is proof of concept and not matured by testing in real environment

The script manages settings using a JSON file (smart_paste_settings.json).
It provides functions to read and write a dictionary of class translations from and to the JSON file.
Class Management:

It defines functions to get a list of existing class names in the document and find the difference between two lists of classes.
After a Paste operation (vs.DoMenuTextByName('Paste', 0)), it identifies new classes.
Dialog Interface:

If the debug_dialog flag is set to True, a predefined list of new classes is used for testing the dialog.
The script creates a dialog that allows the user to interactively manage class translations.
The dialog includes options to paste all into the active class, view classes present in the dictionary, substitute classes in the dictionary but not in the drawing, and handle new classes not yet in the dictionary.
Dialog Functionality:

The dialog provides an organized interface with columns for the original class name, the last translated class, and a dropdown menu for selecting a new class.
It includes color-coding for clarity (blue for in the dictionary, green for in the drawing but not in the dictionary, red for not in the dictionary).
Users can save their settings using a checkbox.
Undo Mechanism:

If the dialog is canceled, the script performs an Undo operation (vs.DoMenuTextByName('Undo', 0)).
Post-Dialog Processing:

After the dialog, if saving settings is enabled, it updates the JSON file with the new class translations.
It handles the class translations for pasted objects, updating their classes according to the user's selections.
It identifies classes to delete based on the difference between new classes and those in the class dictionary.
Overall, the script provides a user-friendly interface for managing class translations, making it easier to handle classes in a Vectorworks document.
'''

import vs
import os
import json

# Set the folder and file paths for the settings
settings_folder = os.path.join(vs.GetFolderPath(-15), 'smart_paste')
settings_file = os.path.join(settings_folder, 'smart_paste_settings.json')

# Set debug mode (True for debugging, False otherwise)
debug_dialog = False

# Get the active class name
active_class_name = vs.ActiveClass()

# Function to read settings from a JSON file


def read_settings(settings_folder, settings_file):
	# Create the settings folder if it doesn't exist
	if not os.path.isfile(settings_folder):
		os.makedirs(settings_folder, exist_ok=True)

	try:
		# Read the class dictionary from the settings file
		with open(settings_file, 'r', encoding='utf-8') as file:
			class_dictionary = json.load(file)

	except:
		# If an error occurs during reading, set an empty dictionary
		class_dictionary = {}

	return class_dictionary

# Function to write settings to a JSON file


def write_settings(settings_folder, settings_file, class_dictionary):
	# Create the settings folder if it doesn't exist
	if not os.path.isfile(settings_folder):
		os.makedirs(settings_folder, exist_ok=True)

	try:
		with open(settings_file, 'w', encoding='utf-8') as file:
			file.write(json.dumps(class_dictionary,
					   ensure_ascii=False, indent=2))

	except:
		# If an error occurs during writing, display an alert dialog
		vs.AlrtDialog('Error writing setting File')

# Function to get all class names in the document


def get_class_names():
	out_classes = []
	num_class = vs.ClassNum()
	for i in range(1, num_class + 1):
		cname = vs.ClassList(i)
		out_classes.append(cname)

	return out_classes

# Function to find the difference between two lists of classes


def class_diff(old_list, new_list):
	return [c_new for c_new in new_list if c_new not in old_list]


# Read existing translation from the settings file
class_dictionary = read_settings(settings_folder, settings_file)

# Get existing class names in the document
existing_classes = get_class_names()

# Perform a Paste operation
vs.DoMenuTextByName('Paste', 0)

# Get existing class names after pasting
actual_classes = get_class_names()

# Get new classes after pasting by finding the difference
new_classes = class_diff(existing_classes, actual_classes)


# class list for testing dialog
if debug_dialog:
	new_classes = [
		"Lorem ipsum",
		"Unknown Class1",
		"Dolor sit am",
		"Consectetur",
		"Adipiscing",
		"ClassNotInDictionary",
		"Elit",
		"Sed do eiusm",
		"Unknown Class2",
		"Tempor",
		"Incididunt ut",
		"Labore et",
		"Dolore magna",
		"Aliqua",
		"Ut enim ad",
		"Minim veniam",
		"Quis nostrud",
		"Exercitation",
		"Ullamco",
		"Laboris",
		"Duis aute",
		"Reprehenderit",
		"Voluptate",
		"Velit esse",
		"Cillum",
		"Excepteur sint",
		"Proident",
		"Culpa qui",
		"Aenean commodo",
		"Massa quis",
		"Enim justo",
		"Rhoncus ut",
		"Integer",
		"Tincidunt",
		"Cras dapibus",
		"Vivamus",
		"Elementum",
		"Aenean vulpu",
		"Aliquam lorem",
		"Phasellus",
		"Fermentum",
		"Viverra quis"
	]

items_left = []
items_middle = []
items_right = []
translate_list = []
saved_settings = {}


def dialog_settings():
	# Control IDs
	kOK = 1
	kCancel = 2

	# UIAskForStringsToReplace
	def CreateDialog():
		# Alignment constants

		# Create the main dialog layout
		dialog = vs.CreateLayout(
			'Class Substitute', False, 'OK', 'Cancel', True)

		# Create controls and set their positions
		vs.CreatePushButton(dialog, 5, 'Paste all in active class.')
		vs.SetFirstLayoutItem(dialog, 5)
		vs.CreateStaticText(dialog, 6, 'new_class present in dictionary', 30)
		vs.SetStaticTextColorN(dialog, 6, 0)
		vs.SetBelowItem(dialog, 5, 6, 0, 1)
		vs.CreateStaticText(dialog, 7, 'substitute class in dictionary but not in drawing', 40)
		vs.SetStaticTextColorN(dialog, 7, 4)
		vs.SetRightItem(dialog, 6, 7, 0, 0)
		vs.CreateStaticText(dialog, 8, 'new class not yet in dictionary', 30)
		vs.SetStaticTextColorN(dialog, 8, 2)
		vs.SetRightItem(dialog, 7, 8, 0, 0)
		vs.CreateStaticText(dialog, 9, 'Existing class manually attached', 30)
		vs.SetStaticTextColorN(dialog, 9, 3)
		vs.SetRightItem(dialog, 8, 9, 0, 0)

		# Script Scope Variable initialization
		max_lines = 20
		num_groups = int(len(new_classes) / max_lines + 1)
		kgroup_list = []
		start_id = item_id = 12
		class_counter = 0
		kgroup_list = []

		for i in range(num_groups):
			# Create a group for each set of classes
			item_counter = 1
			group_name = 'List ' + str(i)
			vs.CreateGroupBox(dialog, item_id, group_name, False)
			kgroup_list.append(item_id)
			last_item = item_id
			item_id += 1

			for c_index in range(class_counter, len(new_classes)):
				c_new = new_classes[c_index]
				if class_counter >= max_lines * (i + 1):
					break

				# Check if the class is in the dictionary and set text styles accordingly
				in_dict = False
				in_dict_butnot_drawing = False

				if c_new in class_dictionary:
					in_dict = True
					last_choice = class_dictionary[c_new]
					if last_choice not in existing_classes:
						in_dict_butnot_drawing = True

				text_style = 2  # 2blue, 3green, 4red, 0black

				if in_dict:
					text_style = 0
					if in_dict_butnot_drawing:
						text_style = 4

				else:
					class_dictionary.update({c_new: active_class_name})
					text_style = 2

				last_choice = class_dictionary[c_new]

				# Create static texts for class names and last choices
				vs.CreateStaticText(dialog, item_id, c_new, 65)
				vs.SetStaticTextColorN(dialog, item_id, text_style)

				if item_id == kgroup_list[-1] + 1:
					vs.SetFirstGroupItem(dialog, kgroup_list[-1], item_id)
				else:
					vs.SetBelowItem(dialog, last_item - 2, item_id, 0, 1)

				last_item = item_id
				item_id += 1

				vs.CreateStaticText(dialog, item_id, last_choice, 65)
				vs.SetStaticTextColorN(dialog, item_id, text_style)
				vs.SetRightItem(dialog, last_item, item_id, 0, 0)

				last_item = item_id
				item_id += 1

				vs.CreateClassPullDownMenu(dialog, item_id, 25)
				vs.SetStaticTextColorN(dialog, item_id, text_style)
				vs.SetRightItem(dialog, last_item, item_id, 0, 0)

				last_item = item_id
				item_id += 1

				item_counter += 3
				class_counter += 1

			item_counter += 1

		# Create a tab control for better organization
		vs.CreateTabControl(dialog, 10)
		vs.SetBelowItem(dialog, 6, 10, 0, 5)

		for id in kgroup_list:
			vs.CreateTabPane(dialog, 10, id)

		# Create a checkbox for saving settings
		last_item = item_id
		item_id += 1
		vs.CreateCheckBox(dialog, item_id, 'Save Settings')
		saved_settings['id_save'] = item_id
		vs.SetBelowItem(dialog, 10, item_id, 0, 2)

		return dialog

	# Dialog handler function for dialog interactions
	def DialogHandler(item, data):
		if item == 12255:  # Enter Dialog
			pass

		if item == 5:
			# Update middle column with the active class name for all classes
			for id_middle in items_middle:
				vs.SetItemText(dialog, id_middle, active_class_name)
				vs.SetStaticTextColorN(dialog, id_middle, 3)
				class_dictionary[vs.GetItemText(
					dialog, id_middle - 1)] = active_class_name

		if item in items_right:
			# Update the right column with the selected class name
			item_text = vs.GetItemText(dialog, item)
			vs.SetItemText(dialog, item - 1, item_text)
			vs.SetStaticTextColorN(dialog, item - 1, 3)

			if item_text not in existing_classes:
				vs.SetStaticTextColorN(dialog, item - 1, 4)

			class_dictionary[vs.GetItemText(dialog, item - 2)] = item_text

		if item == kOK:
			# Save settings if the checkbox is selected
			saved_settings['save'] = vs.GetBooleanItem(
				dialog, saved_settings['id_save'])

			# Store translation information in a list
			for id_left, id_middle, id_right in zip(items_left, items_middle, items_right):
				c_old = vs.GetItemText(dialog, id_left)
				c_new = vs.GetItemText(dialog, id_middle)
				c_last = vs.GetItemText(dialog, id_right)
				translate_list.append([c_old, c_new, c_last])

			return kOK

	# Initialize variables and run the dialog
	result = False
	dialog = CreateDialog()

	if vs.RunLayoutDialogN(dialog, DialogHandler, True) == kOK:
		result = True

	return result


result = True
if len(new_classes) > 0:
	result = dialog_settings()
	if result:
		# Check if saving settings is enabled
		if saved_settings['save']:
			# Write updated settings to the settings file
			write_settings(settings_folder, settings_file, class_dictionary)

		# Initialize a list to store pasted objects
		pasted_objs = []

		# Function to add handles to the list
		def add_handle(h):
			pasted_objs.append(h)

		# Get the active layer and its name
		lh = vs.ActLayer()
		ln = vs.GetLName(lh)

		# Define criteria for selecting objects based on the active layer name
		c = "(INSYMBOL & INOBJECT & NOTINDLVP & NOTINREFDLVP & ((L='ldummy') & (SEL=TRUE)))"
		c = c.replace('ldummy', ln)

		# Use ForEachObject to collect handles of selected objects
		vs.ForEachObject(add_handle, c)

		# Loop through the pasted objects and update their classes
		for obj in pasted_objs:
			old_class = vs.GetClass(obj)
			new_class = class_dictionary.get(old_class, 'None')
			vs.SetClass(obj, new_class)

		# Identify classes to delete
		classes_to_delete = []
		for class_n in new_classes:
			if class_n not in class_dictionary.values():
				classes_to_delete.append(class_n)

		# Delete identified classes
		for class_name in classes_to_delete:
			vs.DelClass(class_name)


if not result:
	vs.DoMenuTextByName('Undo', 0)



For testing purposes this Marionette can be helpful to train the dictionary.



Not really tested in real-application. @matteoluigi you see potential to take over the existing script?


Test Classes.vwx

  • Love 2
Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Answer this question...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...