viewport.activeViewport problem in callback


#1

I am trying to write a small set of functions that link all viewports to the same rotation.
It is working, except that whenever any other window gets focus, max hangs for several minutes. I have narrowed it down to the viewport.activeViewport being set inside the callback timer function. Here is the code, with the offending line marked in orange:

global linkViewports
struct linkViewportsStr
(
	callbackString = "linkViewports.startLinkTimer()",
	linkTimer,
	lastTM = matrix3 0,
	
	fn areMatricesIdentical a b = a[1]==b[1] and a[2]==b[2] and a[3]==b[3] and a[4]==b[4],
	
	fn setCallbacks state =
	(
		format "Settings callbacks to %
" state
		if state then callbacks.addScript #viewportChange callbackString id:#linkViewports
		else callbacks.removeScripts id:#linkViewports
	),
	
	fn doSync =
	(
		format "Entered doSync fn
"
		local masterView = viewport.activeViewport --check which viewport is active (the master)
		local masterTm = viewport.GetTM() --get current viewport transform (for rotation part)
		if not areMatricesIdentical lastTM masterTM do --if the new transform is different than the old transform
		(
			setCallbacks off --we must disable the callback temporarily, otherwise we will get stuck in an infinite loop!
			format "Syncing viewports
"
			lastTM = masterTM
			with redraw off --disable redrawing
			(
				for v = 1 to viewport.numViews do if v!=masterView do --for each viewport except the master
				(
					if viewport.getType index:v != #view_persp_user do continue --ignore orthographic views and cameras
					format "Settings viewport no. %
" v
					viewport.activeViewport = v
					local slaveTm = viewport.GetTM() --get the slave viewport transform (for position part)
					viewport.setTM (matrix3 masterTm.row1 masterTm.row2 masterTm.row3 slaveTM.row4) -- set the transform
				)
				viewport.activeViewport = masterView --return the view to the one we started from	
			)		
			setCallbacks on --reenable the callbacks
		)
	),
	
	fn linkTimerHandler sender args =
	(		
		if mouse.buttonStates.numberSet == 0 do --if we have stopped moving (no mouse buttons are pressed)
		(
			format "Stopping linkTimer
"
			sender.stop() --stop the timer
			linkViewports.doSync() --perform the synchronization
		)
	),	
	
	fn startLinkTimer = 
	(
		if not linkViewports.linkTimer.enabled and mouse.buttonStates.numberSet == 0 do 
		(
			format "Starting linkTimer
"
			linkviewports.linkTimer.start() --start the timer
		)
	),		
	
	fn initializeLinkStruct =
	(
		callbacks.removeScripts id:#linkViewports
		linkTimer = dotNetObject "System.Timers.Timer" 500 --create a timer with 500ms interval
		dotNet.addEventHandler linkTimer "Elapsed" linkTimerHandler --when the timer ticks, run the link callback
	),
	
	_init = initializeLinkStruct() --a variable that executes the initializeLinkStruct function when the struct instance is created
)

linkViewports = linkViewportsStr() --create struct instance
linkViewports.setCallbacks on --turn link on

I am on max2009 64.


#2

why do you use timer? i would use redraw views callback.


#3

As usual you are right.
Without the timer it works fine, even with #viewportChange callback.
I added the timer so it wouldn’t redraw all the time, but I can just add the mousebuttons check in the callback function and it’s ok.

Maybe the problem was that the timer callback runs on a different thread than the 3dsmax UI thread.

Thanks for the help.


#4

Here is the working (and shorter) code if anyone is interested:

global linkViewports
struct linkViewportsStr
(
	callbackString = "linkViewports.doSync()",	
	lastTM = matrix3 0, --a variable to store the last transform
	
	fn areMatricesEqual a b = a[1]==b[1] and a[2]==b[2] and a[3]==b[3] and a[4]==b[4],
	
	fn setLinkActive state =
	(
		callbacks.removeScripts id:#linkViewports
		if state do callbacks.addScript #viewportChange callbackString id:#linkViewports
	),
	
	fn doSync =
	(
		if mouse.buttonStates.numberSet == 0 and viewport.getType() == #view_persp_user do --if the mouse is not pressed and this is a perspective viewport
		(		
			local masterView = viewport.activeViewport --check which viewport is active (the master)
			local masterTm = viewport.GetTM() --get current viewport transform (for rotation part)
			if not areMatricesEqual lastTM masterTM do --if the new transform is different than the old transform
			(
				local offset = masterTm.row4-lastTm.row4 --offset in position since last update
				lastTM = masterTM --save new transform for next update
				setLinkActive off --before changing the viewports, we must disable the callback temporarily, otherwise we will get stuck in an infinite loop!
				with redraw off --disable redrawing
				(
					viewport.activeViewport = 1
					for v = 1 to viewport.numViews do if v!=masterView and viewport.getType index:v == #view_persp_user do --for each perspective viewport except the master
					(
						viewport.activeViewport = v
						local slaveTm = viewport.GetTM() --get the slave viewport transform (for position part)
						viewport.setTM (matrix3 masterTm.row1 masterTm.row2 masterTm.row3 (slaveTm.row4+offset)) -- set the transform
					)
					viewport.activeViewport = masterView --return the view to the one we started from	
				)		
				setLinkActive on --reenable the callbacks
			)
		)
	)	
)

linkViewports = linkViewportsStr() --create struct instance

linkViewports.setLinkActive on --turn link on

#5

timer has to work. i use a timer for some viewport things. but not is in this case.


#6

the only command which causes the problem is setting viewport.activeViewport property.
For example, viewport.GetTM and viewport.setTM do not cause the problem.


#7

i like the method with timer :wink:


unRegisterRedrawViewsCallback syncViews

global ViewHwnd = for w in (windows.getChildrenHWND #max) where w[4] == "ViewPanel" do exit with w[1]
WM_SETREDRAW = 0x000B

global DirtyViewport
global ViewTransform = matrix3 1
global LastView = undefined

fn dirtyViewportDraw = (DirtyViewport = on)
fn syncViews s e = if viewport.numViews > 1 and DirtyViewport and (viewport.getType()) == #view_persp_user do
(
	s.Stop()
	atm = viewport.GetTM()
	ctm = ViewTransform
	av = viewport.activeViewport
	if LastView == av and ((atm[1] != ctm[1]) or (atm[2] != ctm[2]) or (atm[3] != ctm[3])) do
	(
		windows.sendmessage ViewHwnd WM_SETREDRAW 0 1
		
		for v=1 to viewport.numViews where v != av and (viewport.getType index:v) == #view_persp_user do
		(
			viewport.activeViewport = v
			vtm = viewport.GetTM()
			viewport.setTM (translate (rotate (scalematrix vtm.scale) atm.rotation) vtm.pos) 
		)
		viewport.activeViewport = av 
		windows.sendmessage ViewHwnd WM_SETREDRAW 1 1
	)
	LastView = av
	
	ViewTransform = atm
	DirtyViewport = off
	s.Start()
)
registerRedrawViewsCallback dirtyViewportDraw

try
(
	dotnet.removeAllEventHandlers tmr
	tmr.dispose()
) 
catch()

tmr = dotnetobject "Timer"
tmr.Stop()
tmr.Interval = 10
dotnet.addEventHandler tmr "Tick" syncViews
DirtyViewport = on
tmr.Start()



#8

very nice!

You get extra points for not using a c# assembly :wink:


#9

i wanted… but… put a lid on my desire…


#10

do you sync views scale on purpose?


#11

Honsetly, I haven’t given it much thought. Is there ever a naturally occurring case in which the perspective viewport TM scale part is not [1,1,1]?


#12

scale might be any. matrix’s [1] ([2],[3]) is direction*scale. in my sample i don’t sync scale.


#13

Yes, I noticed that. But I’m trying to understand the logic of why it’s important. Can you give an example in which B.scale!=[1,1,1][/B] (other that setting it so by script)?


#14

that’s easy. just try to change a view by wheeling mouse middle button


#15

That is what I tried but the scale stays [1,1,1] for me… only the position changes.


#16

that means i am going off my head.


#17

never mind … it’s paranoia… when i see matrix3 i always split it on translation-rotation-scale


#18

I’ve been breaking my head how to have the viewports maintain their initial position offset. Imagine two viewports looking at similar objects in two different positions. When one orbits around the object, the other should as well.
for some reason, I can not achieve this. I feel like I am making a very obvious mistake but can not find it.

Here is my current code, I’ve implemented Denis’s redraw-disable messages.

global linkViewports	
struct linkViewportsStr
(
	callbackString = "linkViewports.doSync()",
	
	ViewHwnd = (for w in (windows.getChildrenHWND #max) where w[4] == "ViewPanel" collect w[1])[1],
	WM_SETREDRAW = 0x000B,
	
	lastTM, 
	lastView,
	lastFOV,
	lastFD,
	
	fn areMatricesDifferent a b = a[1]!=b[1] or a[2]!=b[2] or a[3]!=b[3] or a[4]!=b[4],
	
	fn registerCallbacks state =
	(
		callbacks.removeScripts #viewportChange id:#linkViewports
		if state do callbacks.addScript #viewportChange callbackString id:#linkViewports
	),
	
	fn setLinkActive state =
	(
		if state then
		(
			lastTM = getViewTm()
			lastView = viewport.activeViewport
			lastFOV = viewport.GetFOV()
			lastFD = viewport.GetFocalDistance()			
		)
		registerCallbacks state
	),
	
	fn doSync =
	(
		if viewport.getType() == #view_persp_user do
		(
			local masterView = viewport.activeViewport
			local masterTm = getviewtm()
			local masterFOV = viewport.GetFOV()
			local masterFD = viewport.GetFocalDistance()
			if lastView==masterView and (lastFOV!=masterFOV or areMatricesDifferent lastTM masterTM) do
			(
				registerCallbacks off
				windows.sendmessage ViewHwnd WM_SETREDRAW 0 1
				for v = 1 to viewport.numViews do if v!=masterView and viewport.getType index:v == #view_persp_user do
				(
					viewport.activeViewport = v
					local slaveTm = inverse (getViewTm())
					local slaveOffset = (slaveTM.pos* masterTM)
					viewport.setTM (translate masterTM slaveOffset)							
						
					--local fPoint = [0,0,-masterFD] * slaveTM					
					--viewport.setFOV masterFOV
					--viewport.setFocalDistance (distance fPoint slaveTM.pos)
				)
				viewport.activeViewport = masterView
				windows.sendmessage ViewHwnd WM_SETREDRAW 1 1
				registerCallbacks on
			)
			lastView=masterView
			lastTM=masterTm
			lastFOV=masterFOV
			lastFD = masterFD
		)
	),
	
	fn init =
	(
		setLinkActive off
		windows.sendmessage ViewHwnd WM_SETREDRAW 1 1 --for testing
	),
	
	_init = init() 
)

linkViewports = linkViewportsStr()


--test scene
(
	delete objects
	teapot pos:[100,0,0] radius:25 wirecolor:red
	teapot pos:[-100,0,0] radius:25 wirecolor:blue
	viewport.resetAllViews()
	viewport.setLayout #layout_2v 
	viewport.activeviewport = 1
	viewport.setType #view_persp_user
	viewport.setFov 45.0
	viewport.setTM (matrix3 [1,0,0] [0,0,-1] [0,1,0] [97.4478,-22.0418,-103.726])
	viewport.setFocalDistance 103.18
	viewport.activeviewport = 2
	viewport.setType #view_persp_user
	viewport.setFov 45.0
	viewport.setTM (matrix3 [1,0,0] [0,0,-1] [0,1,0] [-102.32,-22.0418,-107.547])
	viewport.setFocalDistance 103.18
)
linkViewports.setLinkActive on

#19

look at viewport.rotate method. it seems only chance for you to get what you want.


#20

tried that, but even when specifying correct center point, results come out wrong for some reason…