-- Property Helpers are ingame objects that visualize and(or) modify an object's property. They are requested per property -- through the property metatable field "helper". If the main object's property can be changed from Hedge, the helper should implement -- Update method to receive notification.If modifications of the helper from the ingame editor affect the main object(or the helper) - the helper should -- implement EditorCallback method and should have cfEditorCallback set in order to receive notification. -- (see RelativePos helper for reference). ---------------------------------------------------------------------------------- -- Helper classes definition -- ---------------------------------------------------------------------------------- DefineClass.PropertyHelper = { __parents = { "Object" }, flags = { cfEditorCallback = true }, parent = false, } function PropertyHelper:Init() self:ClearGameFlags(const.gofPermanent) end function PropertyHelper:Create() end function PropertyHelper:Update(obj, value, id) end function PropertyHelper:EditorCallback(action) end function PropertyHelper:GetHelperParent() return self.parent end function PropertyHelper:AddRef(helpers) end --------------------------------------------- --helper functions for creating PropHelpers ------------------------------------------- local function GenericPropHelperCreate(type,info) local no_edit = info.property_meta.no_edit or function() end if GetMap() ~= "" and info.object and not no_edit(info.object, info.property_id) then local marker = _G[type]:new() marker:Create(info.object, info.property_id, info.property_meta, info.property_value, false) return marker end end local CreatePropertyHelpers = { ["sradius"] = function(info,useSecondColor) local helper = PropertyHelper_SphereRadius:new() helper:Create(info.object, info.property_id, info.property_meta, info.property_value, useSecondColor) return helper end, ["srange"] = function(info) local helper = false if info.object:IsKindOf("CObject") then helper = PropertyHelper_SphereRange:new() helper:Create(info.mainObject, info.object, info.property_id, info.property_meta, info.property_value) end return helper end, ["relative_pos"] = function(info) return GenericPropHelperCreate("PropertyHelper_RelativePos",info) end, ["relative_pos_list"] = function(info) return GenericPropHelperCreate("PropertyHelper_RelativePosList",info) end, ["relative_dist"] = function(info) return GenericPropHelperCreate("PropertyHelper_RelativeDist",info) end, ["absolute_pos"] = function(info) local helper_class = info.property_meta and info.property_meta.helper_class or "PropertyHelper_AbsolutePos" return GenericPropHelperCreate(helper_class, info) end, ["terrain_rect"] = function(info) return GenericPropHelperCreate("PropertyHelper_TerrainRect",info) end, ["volume"] = function(info) return GenericPropHelperCreate("PropertyHelper_VolumePicker", info) end, ["box3"] = function(info) local helpers = info.helpers if helpers then local helper = helpers.BoxWidth or helpers.BoxHeight or helpers.BoxDepth if helper then return helper end end local helper = PropertyHelper_Box3:new() helper:Create(info.object) return helper end, ["spotlighthelper"] = function(info) local helpers = info.helpers if helpers then local helper = helpers.ConeInnerAngle if helper then return helper end end local helper = PropertyHelper_SpotLight:new() helper:Create(info.object) return helper end, ["scene_actor_orientation"] = function(info) local main_obj = info.mainObject local parent_helpers = info.helpers if rawget(main_obj, "map") and main_obj.map ~= "All" and ("Maps/" .. main_obj.map .. "/"):lower() ~= GetMap():lower() then return false end for _ , helper in pairs(parent_helpers) do if helper:IsKindOf("PropertyHelper_SceneActorOrientation") then return helper end end local helper = PropertyHelper_SceneActorOrientation:new() helper:Create(info.object, info.property_id, info.property_meta) return helper end, } -- A helper that shows a marker and measures its relative pos to the main object. -- Property must be from type point. DefineClass.PropertyHelper_RelativePos = { __parents = { "Shapeshifter", "EditorVisibleObject", "PropertyHelper" }, entity = "WayPoint", use_object = false, prop_id = false, origin = false, outside_object = false, angle_prop = false, no_z = false, line = false, } function PropertyHelper_RelativePos:Create(obj, prop_id, prop_meta, prop_value) self.parent = obj self.prop_id = prop_id self.use_object = prop_meta.use_object self.origin = prop_meta.helper_origin self.outside_object = prop_meta.helper_outside_object self.angle_prop = prop_meta.angle_prop self.no_z = prop_meta.no_z if self.use_object then self:ChangeEntity( obj:GetEntity() ) self:SetGameFlags(const.gofWhiteColored) self:SetColorModifier( RGB(10, 10, 10)) else local entity = prop_meta.helper_entity if type(entity) == "function" then entity = entity(obj) end if entity then self:ChangeEntity( entity ) else self:ChangeEntity( "WayPoint" ) self:SetColorModifier(RGBA(255,255,0,100)) end end self:Update(obj, prop_value) if prop_meta.helper_scale_with_parent then local _, parent_radius = obj:GetBSphere() local _, helper_radius = self:GetBSphere() self:SetScale(10*parent_radius/Max(helper_radius, 1)) -- 10% of the parent end self:SetEnumFlags(const.efVisible) if prop_meta.color then self:SetColorModifier(prop_meta.color) end end function PropertyHelper_RelativePos:Update(obj, value) obj = obj or self.parent local center, radius = obj:GetBSphere() local rel_pos = point30 if value then if self.outside_object then rel_pos = SetLen(value, Max(value:Len(), radius)) else rel_pos = value end end local origin = self.origin and center or obj:GetVisualPos() local pos = origin + rel_pos if self.no_z then pos = pos:SetInvalidZ() end self:SetPos(pos) if self.angle_prop then self:SetAngle(obj:GetProperty(self.angle_prop)) else self:SetAxis(obj:GetVisualAxis()) self:SetAngle(obj:GetVisualAngle()) end if self.use_object then self:SetScale(self.parent:GetScale()) end if IsValid(self.line) then DoneObject(self.line) end self:DrawLine(origin) end function PropertyHelper_RelativePos:DrawLine(origin) if IsValid(self.line) then DoneObject(self.line) end self.line = PlaceTerrainLine(self:GetPos(), origin) self:Attach(self.line) end function PropertyHelper_RelativePos:EditorCallback(action_id) local parent = self.parent if not parent then return end if action_id == "EditorCallbackMove" then local origin = self.origin and parent:GetBSphere() or parent:GetVisualPos() local pos = self:GetVisualPos() - origin if self.no_z then pos = pos:SetInvalidZ() end parent:SetProperty(self.prop_id, pos) self:DrawLine(origin) elseif action_id == "EditorCallbackRotate" then if self.angle_prop then parent:SetProperty(self.angle_prop, self:GetVisualAngle()) else parent:SetAxis(self:GetVisualAxis()) parent:SetAngle(self:GetVisualAngle()) end elseif action_id == "EditorCallbackScale" then parent:SetScale(self:GetScale()) else return false end return parent end -- A helper that shows a marker for each point and measures their relative pos to the main object. -- Property must be from type point_list. DefineClass.PropertyHelper_RelativePosList = { __parents = { "PropertyHelper" }, markers = false, prop_id = false, origin = false, no_z = false, line = false, } DefineClass.PropertyHelper_RelativePosList_Object = { __parents = { "Shapeshifter", "EditorVisibleObject", "PropertyHelper" }, entity = "WayPoint", obj = false, prop_id = false, prop_id_idx = false, origin = false, line = false } function PropertyHelper_RelativePosList_Object:Done() if IsValid(self.line) then DoneObject(self.line) end self.line = false end function PropertyHelper_RelativePosList_Object:UpdateLine() if IsValid(self.line) then DoneObject(self.line) end self.line = PlaceTerrainLine(self:GetPos(), self.origin) self:Attach(self.line) end function PropertyHelper_RelativePosList_Object:EditorCallback(action_id) local parent = self.obj if not parent then return end if action_id == "EditorCallbackMove" then local origin = self.origin if not origin then return end local pos = self:GetVisualPos() - origin parent[self.prop_id][self.prop_id_idx] = pos parent:SetProperty(self.prop_id, parent[self.prop_id]) self:UpdateLine() else return false end return parent end function PropertyHelper_RelativePosList:Create(obj, prop_id, prop_meta, prop_value) self.parent = obj self.prop_id = prop_id self.origin = prop_meta.helper_origin self.no_z = prop_meta.no_z self:Update(obj, prop_value) end function PropertyHelper_RelativePosList:Done() for i, m in ipairs(self.markers or empty_table) do if IsValid(m) then DoneObject(m) end end self.markers = false end function PropertyHelper_RelativePosList:Update(obj, value) obj = obj or self.parent local center, radius = obj:GetBSphere() local markers = self.markers if not markers then markers = {} self.markers = markers end -- Sync count local pInList = value and #value or 0 local pSpawned = #markers if pInList ~= pSpawned then if pInList < pSpawned then for i = pSpawned, pInList + 1, -1 do local pToDelete = markers[i] markers[i] = nil if IsValid(pToDelete) then DoneObject(pToDelete) end end elseif pInList > 0 then -- less for i = pSpawned + 1, pInList do local newPoint = PlaceObject("PropertyHelper_RelativePosList_Object") newPoint:ChangeEntity("WayPoint") newPoint:SetEnumFlags(const.efVisible) newPoint:SetColorModifier(RGB(125, 55, 0)) newPoint.obj = obj newPoint.prop_id = self.prop_id newPoint.prop_id_idx = i newPoint:AttachText("Point Helper " .. tostring(i)) markers[i] = newPoint end end end local origin = self.origin and center or obj:GetVisualPos() for i, m in ipairs(markers) do local pos = origin + value[i] if self.no_z then pos = pos:SetInvalidZ() end m.origin = origin m:SetPos(pos) m:UpdateLine() end end DefineClass.PropertyHelper_RelativeDist = { __parents = { "Shapeshifter", "EditorVisibleObject", "PropertyHelper" }, entity = "WayPoint", use_object = false, orientation = false, prop_id = false, pos_update_thread = false, rot_update_thread = false, } function PropertyHelper_RelativeDist:Create(obj, prop_id, prop_meta, prop_value) self.parent = obj self.prop_id = prop_id if prop_meta.orientation then local x,y,z = unpack_params(prop_meta.orientation) self.orientation = Normalize(x,y,z) else self.orientation = axis_z end self.use_object = prop_meta.use_object if self.use_object then self:ChangeEntity( obj:GetEntity() ) self:SetGameFlags(const.gofWhiteColored) self:SetColorModifier(RGB(10, 10, 10)) else local entity = prop_meta.helper_entity if type(entity) == "function" then entity = entity(obj) end if entity then self:ChangeEntity( entity ) else self:ChangeEntity( "WayPoint" ) self:SetColorModifier(RGBA(255,255,0,100)) end end self:Update(obj, prop_value) self:SetEnumFlags(const.efVisible) if prop_meta.color then self:SetColorModifier(prop_meta.color) end end function PropertyHelper_RelativeDist:Update(obj, value) DeleteThread(self.pos_update_thread) DeleteThread(self.rot_update_thread) local parent = self.parent local parent_pos = parent:GetVisualPos() local pos = SetLen(parent:GetRelativePoint(self.orientation) - parent_pos, value or 0) -- SetLen to avoid Scale difference self:SetPos(parent_pos + pos) if self.use_object then self:SetAxis(parent:GetVisualAxis()) self:SetAngle(parent:GetVisualAngle()) self:SetScale(parent:GetScale()) end end function PropertyHelper_RelativeDist:EditorCallback(action_id) local parent = self.parent if action_id == "EditorCallbackMove" then -- the target marker is allowed to move, but only its projection on the orientation vector is kept local parent_pos = parent:GetVisualPos() local orient = SetLen(parent:GetRelativePoint(self.orientation) - parent_pos, 4096) -- SetLen to avoid Scale difference local vector = self:GetVisualPos() - parent_pos local new_dist = Dot(orient, vector, 4096) local target_pos = SetLen(orient, new_dist) --[[ DbgClearVectors() DbgAddVector( parent_pos, orient, RGB(255,0,0)) DbgAddVector( parent_pos, vector, RGB(0,255,0)) DbgAddVector( parent_pos, target_pos, RGB(0,255,255)) --]] parent:SetProperty(self.prop_id, new_dist) -- don't change the target_pos immediately to avoid flickering: DeleteThread(self.pos_update_thread) self.pos_update_thread = CreateRealTimeThread( function() Sleep(200) self:SetPos(parent_pos + target_pos) end) elseif action_id == "EditorCallbackScale" then parent:SetScale(self:GetScale()) elseif action_id == "EditorCallbackRotate" then parent:SetScale(self:GetScale()) -- don't change the orientation immediately to avoid flickering: DeleteThread(self.rot_update_thread) self.rot_update_thread = CreateRealTimeThread( function() Sleep(200) self:SetAxis(parent:GetAxis()) self:SetAngle(parent:GetAngle()) end) else --print(action_id) return false end return parent end DefineClass.PropertyHelper_TerrainRect = { __parents = { "PropertyHelper", "EditorVisibleObject" }, entity = "WayPoint", lines = false, step = guim/2, count_x = -1, count_y = -1, color = RGBA(64, 196, 0, 96), z_offset = guim/4, depth_test = false, parent = false, pos = false, prop_id = false, value = false, show_grid = false, value_min = false, value_max = false, value_gran = false, is_one_dim = false, walkable = false, } function PropertyHelper_TerrainRect:Create(obj, prop_id, prop_meta, prop_value) self.prop_id = prop_id self.color = prop_meta.terrain_rect_color self.step = prop_meta.terrain_rect_step self.walkable = prop_meta.terrain_rect_walkable self.show_grid = prop_meta.terrain_rect_grid self.z_offset = prop_meta.terrain_rect_zoffset self.depth_test = prop_meta.terrain_rect_depth_test self.value_min = prop_meta.min self.value_max = prop_meta.max self.value_gran = prop_meta.granularity self.is_one_dim = prop_meta.editor ~= "point" self.parent = obj self:Update(obj, prop_value) self:SetScale(obj:GetScale() * 80 / 100) self:SetColorModifier(self.color) end function PropertyHelper_TerrainRect:DestroyLines() self.count_x = -1 self.count_y = -1 local lines = self.lines or "" for i=1,#lines do if IsValid(lines[i]) then DoneObject(lines[i]) end end self.lines = {} end function PropertyHelper_TerrainRect:CalcValue(obj) obj = obj or self.parent local centered = not obj:HasMember("TerrainRectIsCentered") or obj:TerrainRectIsCentered(self.prop_id) local coef = centered and 2 or 1 local dx, dy = (self:GetVisualPos() - obj:GetVisualPos()):xy() if not centered then dx = Max(0, dx) dy = Max(0, dy) end local value if self.is_one_dim then value = Max(1, coef * Max(abs(dx), abs(dy))) if self.value_min then value = Max(value, self.value_min) end if self.value_max then value = Min(value, self.value_max) end else dx = Max(1, coef*abs(dx)) dy = Max(1, coef*abs(dy)) if self.value_min then dx = Max(dx, self.value_min) dy = Max(dy, self.value_min) end if self.value_max then dx = Min(dx, self.value_max) dy = Min(dy, self.value_max) end if terminal.IsKeyPressed(const.vkAlt) then local v = Max(dx, dy) value = point(v, v) else value = point(dx, dy) end end if self.value_gran then value = round(value, self.value_gran) end return value end function PropertyHelper_TerrainRect:Update(obj, value) obj = obj or self.parent if not IsValid(obj) then return end if obj:HasMember("TerrainRectIsEnabled") and not obj:TerrainRectIsEnabled(self.prop_id) then self:ClearEnumFlags(const.efVisible) self:DestroyLines() self.pos = false return end self:SetEnumFlags(const.efVisible) local pos = obj:GetVisualPos() local my_pos = self:IsValidPos() and self:GetVisualPos() or pos local centered = not obj:HasMember("TerrainRectIsCentered") or obj:TerrainRectIsCentered(self.prop_id) local dont_move if not value then value = self:CalcValue(obj) dont_move = true end if self.pos == pos and self.value == value and self.centered == centered then return end self.centered = centered self.pos = pos self.value = value local count_x, count_y if self.step <= 0 then count_x = 2 count_y = 2 elseif IsPoint(value) then count_x = Min(100, Max(2, 1 + MulDivRound(2, value:x(), self.step))) count_y = Min(100, Max(2, 1 + MulDivRound(2, value:y(), self.step))) else local count = Min(100, Max(2, 1 + MulDivRound(2, value, self.step))) count_x = count count_y = count end if count_x ~= self.count_x or count_y ~= self.count_y then self:DestroyLines() self.count_x = count_x self.count_y = count_y end local ox, oy, oz = pos:xyz() local color = self.color local offset = self.z_offset local depth_test = self.depth_test local lines = self.lines local walkable = self.walkable local grid = {} local offset_x, offset_y if IsPoint(value) then offset_x, offset_y = value:xy() else offset_x, offset_y = value, value end local startx, starty = ox, oy if centered then if not dont_move then offset_x = abs(offset_x) offset_y = abs(offset_y) end offset_x = offset_x / 2 offset_y = offset_y / 2 startx, starty = ox - offset_x, oy - offset_y end local endx, endy = ox + offset_x, oy + offset_y local mapw, maph = terrain.GetMapSize() local height_tile = const.HeightTileSize endx = Clamp(endx, 0, mapw - height_tile - 1) endy = Clamp(endy, 0, maph - height_tile - 1) startx = Clamp(startx, 0, mapw - height_tile - 1) starty = Clamp(starty, 0, maph - height_tile - 1) if not dont_move then self:SetPos(point(endx, endy)) end for yi = 1,count_y do local y = starty + MulDivRound(endy - starty, yi - 1, count_y - 1) local row = {} for xi = 1,count_x do local x = startx + MulDivRound(endx - startx, xi - 1, count_x - 1) local z = terrain.GetHeight(x, y) if walkable then z = Max(z, GetWalkableZ(x, y)) end row[xi] = point(x, y, z + offset) end grid[yi] = row end local li = 1 local function SetNextLinePoints(points) local line = lines[li] if not line then line = Polyline:new() line:SetMeshFlags(const.mfWorldSpace) line:SetDepthTest(depth_test) lines[li] = line obj:Attach(line, obj:GetSpotBeginIndex("Origin")) end line:SetMesh(points) li = li + 1 end for yi = 1,count_y do if self.show_grid or yi == 1 or yi == count_y then local points = pstr("") for xi = 1,count_x do points:AppendVertex(grid[yi][xi], color) end SetNextLinePoints(points) end end for xi = 1,count_x do if self.show_grid or xi == 1 or xi == count_x then local points = pstr("") for yi = 1,count_y do points:AppendVertex(grid[yi][xi], color) end SetNextLinePoints(points) end end end function PropertyHelper_TerrainRect:EditorCallback(action_id) if not IsValid(self.parent) then return end if action_id == "EditorCallbackMove" then self.parent:SetProperty(self.prop_id, self:CalcValue()) self:Update() return self.parent end end function PropertyHelper_TerrainRect:Done() self:DestroyLines() end DefineClass.PropertyHelper_AbsolutePos = { __parents = { "Shapeshifter", "EditorVisibleObject", "PropertyHelper" }, entity = "WayPoint", use_object = false, prop_id = false, angle_prop = false, } function PropertyHelper_AbsolutePos:Create(obj, prop_id, prop_meta, prop_value) self.parent = obj self.prop_id = prop_id self.use_object = prop_meta.use_object if self.use_object then self:ChangeEntity( obj:GetEntity() ) self:SetGameFlags(const.gofWhiteColored) self:SetColorModifier( RGB(255, 10, 10)) else local entity = prop_meta.helper_entity if type(entity) == "function" then entity = entity(obj) end if entity then self:ChangeEntity( entity ) else self:ChangeEntity( "WayPoint" ) self:SetColorModifier(RGBA(255,255,0,100)) end end if obj:HasMember("OnHelperCreated") then obj:OnHelperCreated(self) end local angle = prop_meta.angle_prop and obj:GetProperty(prop_meta.angle_prop) if angle then self.angle_prop = prop_meta.angle_prop self:SetAngle(angle) end self:Update(obj, prop_value) self:SetEnumFlags(const.efVisible) if prop_meta.color then self:SetColorModifier(prop_meta.color) end end function PropertyHelper_AbsolutePos:AddRef(helpers) if self.angle_prop then helpers[self.angle_prop] = self end end function PropertyHelper_AbsolutePos:Update(obj, value) if type(value) ~= "number" then if not value or value == InvalidPos() then value = GetVisiblePos() end self:SetPos(value) else self:SetAngle(value) end end function PropertyHelper_AbsolutePos:EditorCallback(action_id) local parent = self.parent if parent then parent:SetProperty(self.prop_id, self:GetVisualPos()) if self.angle_prop then parent:SetProperty(self.angle_prop, self:GetVisualAngle()) end end return parent end -- A helper that is cut scene editor specific. Only one is created per object and manages -- pos, axis and angle per selected actor. -- Only usable by ActionAnimation class (currently). DefineClass.PropertyHelper_SceneActorOrientation = { __parents = { "Shapeshifter", "EditorVisibleObject", "PropertyHelper" }, actor_entity = false, } function PropertyHelper_SceneActorOrientation:Create(parent, prop_id, prop_meta ) self.parent = parent self:SetGameFlags(const.gofWhiteColored) self:Update() EditorActivate() self:SetEnumFlags(const.efVisible) self:SetRealtimeAnim(true) end function PropertyHelper_SceneActorOrientation:Update(obj) local parent = self.parent local actor_entity = parent:GetActorEntity() if actor_entity and actor_entity ~= self.actor_entity then self:ChangeEntity(actor_entity) self.actor_entity = actor_entity end if self.actor_entity and parent.pos ~= InvalidPos() then self:SetPos(parent.pos) self:SetAxis(parent.axis) self:SetAngle(parent.angle) if rawget(parent, "animation") and parent.animation ~= "" then self:SetStateText(parent.animation) end if rawget(parent, "animation") then self:SetStateText(parent.animation) end else self:DetachFromMap() end end function PropertyHelper_SceneActorOrientation:EditorCallback(action_id) if not self.parent then return end if action_id == "EditorCallbackMove" or action_id == "EditorCallbackRotate" and self.actor_entity then local parent = self.parent parent.pos = self:GetPos() parent.axis = self:GetAxis() parent.angle = self:GetAngle() return parent end end -- A helper that visualizes a single radius with one code renderable sphere. -- Property must be from type number. DefineClass.PropertyHelper_SphereRadius = { __parents = { "PropertyHelper", "EditorObject" }, sphere = false, color = false, square = false, square_divider = 1 } function PropertyHelper_SphereRadius:Create(obj, prop_id, prop_meta, prop_value, useSecondColor) if prop_meta.square then self.square = true if prop_meta.square_divider then self.square_divider = prop_meta.square_divider end end local radius if self.square then radius = prop_value * prop_value / self.square_divider else radius = prop_value end if useSecondColor and prop_meta.color2 then self.color = prop_meta.color2 elseif prop_meta.color then self.color = prop_meta.color else self.color = RGB(255,255,255) end local sphere = CreateSphereMesh(radius, self.color) sphere:SetDepthTest(true) obj:Attach(sphere, obj:GetSpotBeginIndex("Origin")) self.parent = obj self.sphere = sphere end function PropertyHelper_SphereRadius:Update(obj, value) local radius if self.square then radius = value * value / self.square_divider else radius = value end self.sphere:SetMesh(CreateSphereVertices(radius, self.color)) end function PropertyHelper_SphereRadius:EditorEnter() if IsValid(self.sphere) then self.sphere:SetEnumFlags(const.efVisible) end end function PropertyHelper_SphereRadius:EditorExit() if IsValid(self.sphere) then self.sphere:ClearEnumFlags(const.efVisible) end end function PropertyHelper_SphereRadius:Done() if IsValid(self.parent) and IsValid(self.sphere) then self.sphere:Detach() self.sphere:delete() end end -- A helper that visualizes numerical range with two spheres' radiuses. -- Property must be from type table - { from = , to = } DefineClass.PropertyHelper_SphereRange = { __parents = { "PropertyHelper" }, sphere_from = false, sphere_to = false, } function PropertyHelper_SphereRange:Create(main_obj, obj, prop_id, prop_meta, prop_value) local fromInfo = { mainObject = main_obj, object = obj, property_id = prop_id, property_meta = prop_meta, property_value = prop_value.from, } local toInfo = table.copy(fromInfo) toInfo.property_value = prop_value.to self.sphere_from = CreatePropertyHelpers["sradius"](fromInfo,false) self.sphere_to = CreatePropertyHelpers["sradius"](toInfo,true) end function PropertyHelper_SphereRange:Update(obj, value) if IsValid(self.sphere_from) and IsValid(self.sphere_to) then self.sphere_from:Update(obj, value.from) self.sphere_to:Update(obj, value.to) end end function PropertyHelper_SphereRange:Done() if IsValid(self.sphere_from) and IsValid(self.sphere_to) then DoneObject(self.sphere_from) DoneObject(self.sphere_to) end end ----------------------------------------- custom helper for box lights ----------------------------------------- DefineClass.PropertyHelper_Box3 = { __parents = { "PropertyHelper" }, box = false, } function PropertyHelper_Box3:Create(parent_obj) self.parent = parent_obj self.box = PlaceObject("Mesh") self.box:SetDepthTest(true) self.box:SetShader(ProceduralMeshShaders.mesh_linelist) parent_obj:Attach(self.box, parent_obj:GetSpotBeginIndex("Origin")) self:Update() end function PropertyHelper_Box3:Update(obj, value) local width = self.parent:GetProperty("BoxWidth") or guim local height = self.parent:GetProperty("BoxHeight") or guim local depth = self.parent:GetProperty("BoxDepth") or guim width = width / 2 height = height / 2 depth = -depth local p_pstr = pstr("") local function AddPoint(x,y,z) p_pstr:AppendVertex(point(x*width, y*height, z*depth)) end AddPoint(-1,-1,0) AddPoint(-1,1,0) AddPoint(1,-1,0) AddPoint(1,1,0) AddPoint(-1,-1,0) AddPoint(1,-1,0) AddPoint(-1,1,0) AddPoint(1,1,0) AddPoint(-1,-1,1) AddPoint(-1,1,1) AddPoint(1,-1,1) AddPoint(1,1,1) AddPoint(-1,-1,1) AddPoint(1,-1,1) AddPoint(-1,1,1) AddPoint(1,1,1) AddPoint(-1,-1,0) AddPoint(-1,-1,1) AddPoint(-1,1,0) AddPoint(-1,1,1) AddPoint(1,-1,0) AddPoint(1,-1,1) AddPoint(1,1,0) AddPoint(1,1,1) self.box:SetMesh(p_pstr) end function PropertyHelper_Box3:Done() if IsValid(self.box) then DoneObject(self.box) end end ------------------------------- DefineClass.PropertyHelper_VolumePicker = { __parents = { "PropertyHelper" }, box = false, } function PropertyHelper_VolumePicker:Create(parent_obj, prop_id, prop_meta, prop_value) self.parent = parent_obj self:Update(parent_obj, prop_value) end function PropertyHelper_VolumePicker:Update(obj, value) local target = value and value.box self.box = PlaceBox(target or box(point(0,0,0), point(0,0,0)), RGBA(255, 255, 0, 255), self.box) end function PropertyHelper_VolumePicker:Done() if IsValid(self.box) then DoneObject(self.box) end end ----------------------------------------- custom helper for spot lights ----------------------------------------- DefineClass.PropertyHelper_SpotLight = { __parents = { "PropertyHelper" }, box = false, } function PropertyHelper_SpotLight:Create(parent_obj) self.parent = parent_obj self.box = PlaceObject("Mesh") self.box:SetDepthTest(true) self.box:SetShader(ProceduralMeshShaders.mesh_linelist) parent_obj:Attach(self.box, parent_obj:GetSpotBeginIndex("Origin")) self:Update() end function BuildMeshCone(points_pstr, radius, angle) local rad2 = radius * radius local r = radius * sin(angle*60 / 2) / 4096 local a = r*866/1000 local b = r*2/3 local c = r/2 local d = r/3 local e = r*577/1000 local function addpt(x,y) points_pstr:AppendVertex(point(x, y, -sqrt(rad2 - x*x - y*y))) end local function addcenter() points_pstr:AppendVertex( point30) end local function quadrant(x,y) addpt(x*0,y*0) addpt(x*d,y*e) addpt(x*d,y*e) addpt(x*b,y*0) addpt(x*b,y*0) addpt(x*a,y*c) addpt(x*a,y*c) addpt(x*r,y*0) addpt(x*d,y*e) addpt(x*a,y*c) addpt(x*0,y*r) addpt(x*d,y*e) addpt(x*d,y*e) addpt(x*c,y*a) addpt(x*c,y*a) addpt(x*a,y*c) addpt(x*0,y*r) addpt(x*c,y*a) addpt(x*a,y*c) addcenter() addpt(x*c,y*a) addcenter() end local function semicircle(x) quadrant(x,1) quadrant(x,-1) addpt(0,0) addpt(x*b,0) addpt(x*b,0) addpt(x*r,0) addpt(x*r,0) addcenter() end semicircle(1) semicircle(-1) addpt(d,e) addpt(-d,e) addpt(d,-e) addpt(-d,-e) addpt(0,r) addcenter() addpt(0,-r) addcenter() return points_pstr end function PropertyHelper_SpotLight:Update(obj, value) local p_pstr = pstr("") local radius = self.parent:GetProperty("AttenuationRadius") or 5000 local spot_inner_angle = self.parent:GetProperty("ConeInnerAngle") or 45 local spot_outer_angle = self.parent:GetProperty("ConeOuterAngle") or 90 p_pstr = BuildMeshCone(p_pstr, radius, spot_inner_angle) p_pstr = BuildMeshCone(p_pstr, radius, spot_outer_angle) self.box:SetMesh(p_pstr) end function PropertyHelper_SpotLight:Done() if IsValid(self.box) then DoneObject(self.box) end end ---------------------------------------------------------------------------------- -- PropertyHelpers Management System -- ---------------------------------------------------------------------------------- MapVar("PropertyHelpers", {}) MapVar("PropertyHelpers_Refs",{}) function SelectHelperObject(helper_object, no_camera_move) if not IsValid(helper_object) then return end if helper_object:IsValidPos() then EditorActivate() editor:ClearSel() editor.AddToSel({helper_object}) if not no_camera_move then ViewObject(helper_object) end end end -- Helper objects destructor function local function PropertyHelpers_DoneHelpers(object) local helpers = PropertyHelpers[object] for _ , helper in pairs(helpers) do if IsValid(helper) then DoneObject(helper) end end PropertyHelpers_Refs[object] = nil PropertyHelpers[object] = nil end -- Rebuilds helper objects references (usually when window is closed and window ids are changed) function PropertyHelpers_RebuildRefs(ignore_ged) if not PropertyHelpers then return end PropertyHelpers_Refs = {} if IsEditorActive() then return end for i, ged in pairs(GedConnections) do if not ignore_ged or ged == ignore_ged then local objects = ged:GetMatchingBoundObjects({ props = GedGetProperties, values = GedGetValues }) for key, object in ipairs(objects) do if object and PropertyHelpers[object] then local ref_table = PropertyHelpers_Refs[object] or {} table.insert(ref_table, i) PropertyHelpers_Refs[object] = ref_table end end end end end -- Creates helpers for object if not already referenced in other window function PropertyHelpers_Init(obj, ged) if GetMap() == "" or not PropertyHelpers then return end local objects = { obj } local helpers_created = false for i = 1, #objects do local object = objects[i] if IsKindOf(object, "AutoAttachRule") and IsKindOf(g_Classes[object.attach_class], "Light") then local demo_obj = GedAutoAttachDemos[ged] if demo_obj then if (object.required_state or "") ~= "" then demo_obj:SetAutoAttachMode(object.required_state) end end end if PropertyHelpers[object] then if not table.find(PropertyHelpers_Refs[object], ged) then table.insert(PropertyHelpers_Refs[object], ged) end local helpers = PropertyHelpers[object] local selected = {} for _, helper in pairs(helpers) do if not selected[helper] and helper:IsKindOf("PropertyHelper_SceneActorOrientation") then selected[helper] = true SelectHelperObject(helper, not "no_camera_move") end end elseif IsKindOf(object, "PropertyObject") then local helpers = false local properties = object:GetProperties() for i=1, #properties do local property = properties[i] local property_id = property.id if not IsKindOf(object, "GedMultiSelectAdapter") and IsValid(object) then local no_edit = property.no_edit if type(no_edit) == "function" then no_edit = no_edit(object, property_id) end if not no_edit and property.helper then helpers = helpers or {} local property_value = object:GetProperty(property_id) assert(property_value ~= nil, "Could not get property value for "..tostring(property_id)) local info = { object = object, property_id = property_id, property_meta = property, property_value = property_value, helpers = helpers, } local helper_object = false if CreatePropertyHelpers[property.helper] then helper_object = CreatePropertyHelpers[property.helper](info) else assert(false, "Unknown property helper requested" .. (property.helper or "")) end if helper_object then if property.helper == "scene_actor_orientation" and obj == object then SelectHelperObject(helper_object, not "no_camera_move") end helpers[property_id] = helper_object helper_object:AddRef(helpers) local idx = editor.GetLockedCollectionIdx() if idx ~= 0 then helper_object:SetCollectionIndex(idx) end end end end end if helpers then PropertyHelpers[object] = helpers PropertyHelpers_Refs[object] = { ged } if object:HasMember("AdjustHelpers") then object:AdjustHelpers() end helpers_created = true end end end if helpers_created then UpdateCollectionsEditor() end end -- Destroys helpers for object if this window is its last reference function PropertyHelpers_Done(object, window_id) local objects = { object } for i = 1, #objects do local object = objects[i] if PropertyHelpers and PropertyHelpers[object] then local helpers_refs = PropertyHelpers_Refs[object] assert(helpers_refs,"Object property helpers already destroyed") table.remove_value(helpers_refs, window_id) if #helpers_refs == 0 then PropertyHelpers_DoneHelpers(object) end end end end function PropertyHelpers_Refresh(object) PropertyHelpers_UpdateAllHelpers(object) local prop_helpers = {} for object, geds in pairs(PropertyHelpers_Refs) do prop_helpers[object] = table.copy(geds) end for object, ged in pairs(prop_helpers) do if GedConnections[ged.ged_id] then PropertyHelpers_Done(object, ged) end end for object, ged in pairs(prop_helpers) do if GedConnections[ged.ged_id] then PropertyHelpers_Init(object, ged) end end end function PropertyHelpers_ViewHelper(object, prop_id, select) local helper_object = PropertyHelpers and PropertyHelpers[object] and PropertyHelpers[object][prop_id] or false if helper_object and GetMap() ~= "" then EditorActivate() if select and not editor.IsSelected(helper_object) then editor.AddToSel({helper_object}) end ViewObject(helper_object) end end function PropertyHelpers_GetHelperObject(object, prop_id) local helper_object = PropertyHelpers and PropertyHelpers[object] and PropertyHelpers[object][prop_id] or false return helper_object end -- Notifies each helper for property changes in the Property Editor function OnMsg.GedPropertyEdited(ged_id, object, prop_id, old_value) local helpers = PropertyHelpers and PropertyHelpers[object] if not helpers then return end local prop_value = object:GetProperty(prop_id) assert(prop_value ~= nil, "Could not get property value for prop: "..tostring(prop_id)) local prop_metadata = object:GetPropertyMetadata(prop_id) local prop_helper = helpers[prop_id] if prop_helper then prop_helper:Update(object, prop_value, prop_id) end end function PropertyHelpers_UpdateAllHelpers(object) local helpers = PropertyHelpers[object] if not helpers then return end for prop_id, prop_helper in pairs(helpers) do if prop_helper then prop_helper:Update(object, object:GetProperty(prop_id), prop_id) end end end -- Rebuilds helper refs (windows_ids are changed at window close) and calls destroy function -- for unreferenced helper objects function PropertyHelpers_RemoveUnreferenced(ignore_ged_instance) PropertyHelpers_RebuildRefs(ignore_ged_instance) for object, _ in pairs(PropertyHelpers or empty_table) do if not PropertyHelpers_Refs[object] then PropertyHelpers_DoneHelpers(object) end end end function OnMsg.GedOnEditorSelect(obj, selected, ged) if selected then PropertyHelpers_Init(obj, ged) else PropertyHelpers_RemoveUnreferenced(ged) end end function OnMsg.GameExitEditor() PropertyHelpers_RemoveUnreferenced() end local PropertyHelpers_LastAutoUpdate = 0 local PropertyHelpers_UpdateThread = false local PropertyHelpers_ModifiedObjects = {} local function HandleAutoUpdate(object, action_id) -- if the property helper has been changed from the ingame editor if object:IsKindOf("PropertyHelper") then local changed_object = object:EditorCallback(action_id) if changed_object then ObjModified(changed_object) end -- if the main object has been changed from the ingame editor - update its helpers elseif PropertyHelpers[object] then for property_id , helper in pairs(PropertyHelpers[object]) do helper:Update(object, object:GetProperty(property_id), property_id) end end --else object not relevant to PropertyHelpers System end -- Notifies helpers that implement editor callback function for changes from the ingame editor. -- Some updates are skipped(Hedge's column update speed is much slower than msg feedback). -- Thread ensures that a final update is made. function OnMsg.EditorCallback(action_id, objects, ...) local time_now = RealTime() if time_now - PropertyHelpers_LastAutoUpdate < 100 then DeleteThread(PropertyHelpers_UpdateThread) PropertyHelpers_UpdateThread = CreateRealTimeThread(function() Sleep(100) for objs, action_id in pairs(PropertyHelpers_ModifiedObjects) do for _, o in ipairs(objs) do if IsValid(o) then -- when action_id is EditorCallbackDelete, o can be invalid HandleAutoUpdate(o, action_id) end end end table.clear(PropertyHelpers_ModifiedObjects) PropertyHelpers_LastAutoUpdate = RealTime() end) PropertyHelpers_ModifiedObjects[objects] = action_id return end PropertyHelpers_LastAutoUpdate = time_now for _, o in ipairs(objects) do HandleAutoUpdate(o, action_id) end end