Try Creating a Part Instance in the Beggining of the Script and Setting its LookVector To the Normal with
CFrame.LookAt(Position,SamePosition+Normal) and then Getting that parts UpVector
I already explained why that wouldn’t work.
The part would end up looking like this.
It will only look like this
If you have defined the upvector as well
I just spent a while and got this
local CRay = workspace:Raycast(workspace.RayCastInstance.Position,Vector3.new(-50,-0,0))
if CRay then
local GCF = Instance.new("Part") --Get CFrame Part
GCF.CFrame = CFrame.new(GCF.Position,GCF.Position+CRay.Normal)
workspace.RayCastInstance.CFrame =
CFrame.fromMatrix(workspace.RayCastInstance.Position,GCF.CFrame.UpVector,CRay.Normal)
wait()
GCF:Destroy()
end
Got it. I think this is what you are asking for, anyways. Basically, the plan is:
- Find two vectors that are orthogonal to each other, and parallel to the surface
- Find which one is closer to the world Y axis and choose that as the surface Y axis
- Compute the surface X axis based on that and the surface normal
- We have the CFrame of the surface now, we can calculate the size of the part in “surface space” by doing (size of part in object space) → to world → to surface
This gives the following result:
This code was placed in StarterCharacterScripts
:
local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
local p = Instance.new("Part")
p.CanCollide = false
p.Anchored = true
p.Transparency = 0.5
p.BrickColor = BrickColor.Red()
local decal = Instance.new("Decal")
decal.Texture = "http://www.roblox.com/asset/?id=67616959" -- some random "this side up" image
decal.Face = Enum.NormalId.Back -- always the back face
decal.Parent = p
p.Parent = player.Character or player.CharacterAdded:Wait()
local function GetWorldOrientedSurface(part: BasePart, normalId: NormalId)
local cf = part.CFrame
local n = Vector3.fromNormalId(normalId)
local nWorld = cf:VectorToWorldSpace(n)
-- pick arbitrary vector parallel to one of the edges
local x = cf.LookVector
if math.abs(x:Dot(nWorld)) > 0.9999 then
x = cf.RightVector -- for front/back face
end
-- calculate another vector orthongal to the first on the surface
local y = nWorld:Cross(x)
-- flip up if pointing down
x = math.sign(x.Y) * x
y = math.sign(y.Y) * y
-- choose which one is "more up" as the y axis
y = y.Y > x.Y and y or x
-- recalculate x axis based on that y axis vector
x = y:Cross(nWorld)
-- get position on surface
local partSizeNormalAxisOnly = part.Size * n -- e.g. (3, 4, 5) * (0, 1, 0) -> (0, 4, 0)
local partSizeInNormal = partSizeNormalAxisOnly.X + partSizeNormalAxisOnly.Y + partSizeNormalAxisOnly.Z -- e.g. (0, 4, 0) -> 4
local surfacePos = part.Position + nWorld * math.abs(partSizeInNormal) / 2
-- put together cframe matrix
local surfaceCFrame = CFrame.fromMatrix(surfacePos, x, y, nWorld)
-- get width of part in direction of x and y
local sizeInWorldSpace = part.CFrame:VectorToWorldSpace(part.Size)
local sizeInSurfaceSpace = surfaceCFrame:VectorToObjectSpace(sizeInWorldSpace)
-- correct for negatives
sizeInSurfaceSpace = Vector3.new(
math.abs(sizeInSurfaceSpace.X),
math.abs(sizeInSurfaceSpace.Y),
math.abs(sizeInSurfaceSpace.Z))
return surfaceCFrame, sizeInSurfaceSpace
end
game:GetService("RunService").Stepped:Connect(function()
local part = mouse.Target
if part then
local cframe, size = GetWorldOrientedSurface(part, mouse.TargetSurface)
-- size.Z is the depth of the part relative to the surface, but we don't care about that
p.Size = Vector3.new(size.X, size.Y, 0.1)
p.CFrame = cframe
end
end)
Edit: more tests:
It seems to fail on the top and bottom faces if they’re axis-aligned. Working on it
Thanks for the immense effort, but my brain is a bit fried and I’m having trouble implementing your function into my build script
local e = true
local qr
local tb
local mou = game.Players.LocalPlayer:GetMouse()
local function raysurfaceface(e)
local dotY = e:Dot(Vector3.new(0,1,0))
local dotX = e:Dot(Vector3.new(1,0,0))
local dotZ = e:Dot(Vector3.new(0,0,1))
local surface
if (math.abs(dotY) >= 0.9) then
if (dotY < 0) then
surface = Enum.NormalId.Bottom
else
surface = Enum.NormalId.Top
end
elseif (math.abs(dotX) >= 0.9) then
if (dotX < 0) then
surface = Enum.NormalId.Left
else
surface = Enum.NormalId.Right
end
else
if (dotZ < 0) then
surface = Enum.NormalId.Front
else
surface = Enum.NormalId.Back
end
end
return surface
end
local function vec3Func(f, ...)
local x, y, z = {}, {}, {}
for i, v in next, {...} do
x[i], y[i], z[i] = v.x, v.y, v.z
end
return Vector3.new(f(unpack(x)), f(unpack(y)), f(unpack(z)))
end
local function getRotationBetween(u, v, axis)
local dot, uxv = u:Dot(v), u:Cross(v)
if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end
return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot)
end
local function getSurfaceCF(part, lnormal)
local pcf, size2 = part.CFrame, part.Size/2
local transition = getRotationBetween(Vector3.new(0,0,1), lnormal, Vector3.new(0,1,0))
local size = vec3Func(math.abs, transition:VectorToWorldSpace(part.Size))
return (pcf * transition) * CFrame.new(size/2 * Vector3.new(0, 0, 1)), size
end
local function boundingbox(size,cf)
local a = {
Vector3.new(0,0,0)+cf.RightVector.Unit*size.X/2+cf.UpVector.Unit*size.Y/2+cf.LookVector.Unit*size.Z/2,
Vector3.new(0,0,0)+cf.RightVector.Unit*size.X/2+cf.UpVector.Unit*size.Y/2-cf.LookVector.Unit*size.Z/2,
Vector3.new(0,0,0)+cf.RightVector.Unit*size.X/2-cf.UpVector.Unit*size.Y/2+cf.LookVector.Unit*size.Z/2,
Vector3.new(0,0,0)+cf.RightVector.Unit*size.X/2-cf.UpVector.Unit*size.Y/2-cf.LookVector.Unit*size.Z/2,
Vector3.new(0,0,0)-cf.RightVector.Unit*size.X/2+cf.UpVector.Unit*size.Y/2+cf.LookVector.Unit*size.Z/2,
Vector3.new(0,0,0)-cf.RightVector.Unit*size.X/2+cf.UpVector.Unit*size.Y/2-cf.LookVector.Unit*size.Z/2,
Vector3.new(0,0,0)-cf.RightVector.Unit*size.X/2-cf.UpVector.Unit*size.Y/2+cf.LookVector.Unit*size.Z/2,
Vector3.new(0,0,0)-cf.RightVector.Unit*size.X/2-cf.UpVector.Unit*size.Y/2-cf.LookVector.Unit*size.Z/2
}
local minlpx = 0
local minlpy = 0
local minlpz = 0
local maxlpx = 0
local maxlpy = 0
local maxlpz = 0
for i=1,#a do
if a[i].X<minlpx then
minlpx=a[i].X
end
end
for i=1,#a do
if a[i].Y<minlpy then
minlpy=a[i].Y
end
end
for i=1,#a do
if a[i].Z<minlpz then
minlpz=a[i].Z
end
end
for i=1,#a do
if a[i].X>maxlpx then
maxlpx=a[i].X
end
end
for i=1,#a do
if a[i].Y>maxlpy then
maxlpy=a[i].Y
end
end
for i=1,#a do
if a[i].Z>maxlpz then
maxlpz=a[i].Z
end
end
return Vector3.new(minlpx,minlpy,minlpz), Vector3.new(maxlpx,maxlpy,maxlpz)
end
local function getSurfaceSize(CRay)
local List = {"UpVector","LookVector","RightVector"}
local List2 = {}
local Sides = {{"X","Y","Z"},{"X","Z","Y"},{"Y","X","Z"}}
for count = 1,6,1 do
local modi = -1+(count%2)*2
List2[CRay.Instance.CFrame[List[math.ceil(count/2)]]*modi] = Sides[math.ceil(count/2)]
end
local Size = CRay.Instance.Size
local N = List2[CRay.Normal]
return Vector3.new(Size[N[1]],Size[N[2]],Size[N[3]])
end
local function GetWorldOrientedSurface(part, normalId)
local cf = part.CFrame
local n = Vector3.fromNormalId(normalId)
local nWorld = cf:VectorToWorldSpace(n)
-- pick arbitrary vector parallel to one of the edges
local x = cf.LookVector
if math.abs(x:Dot(nWorld)) > 0.9999 then
x = cf.RightVector -- for front/back face
end
-- calculate another vector orthongal to the first on the surface
local y = nWorld:Cross(x)
-- flip up if pointing down
x = math.sign(x.Y) * x
y = math.sign(y.Y) * y
-- choose which one is "more up" as the y axis
y = y.Y > x.Y and y or x
-- recalculate x axis based on that y axis vector
x = y:Cross(nWorld)
-- get position on surface
local partSizeNormalAxisOnly = part.Size * n -- e.g. (3, 4, 5) * (0, 1, 0) -> (0, 4, 0)
local partSizeInNormal = partSizeNormalAxisOnly.X + partSizeNormalAxisOnly.Y + partSizeNormalAxisOnly.Z -- e.g. (0, 4, 0) -> 4
local surfacePos = part.Position + nWorld * math.abs(partSizeInNormal) / 2
-- put together cframe matrix
local surfaceCFrame = CFrame.fromMatrix(surfacePos, x, y, nWorld)
-- get width of part in direction of x and y
local sizeInWorldSpace = part.CFrame:VectorToWorldSpace(part.Size)
local sizeInSurfaceSpace = surfaceCFrame:VectorToObjectSpace(sizeInWorldSpace)
-- correct for negatives
sizeInSurfaceSpace = Vector3.new(
math.abs(sizeInSurfaceSpace.X),
math.abs(sizeInSurfaceSpace.Y),
math.abs(sizeInSurfaceSpace.Z))
return surfaceCFrame
end
local tb = Instance.new("Part",workspace)
tb.Anchored=true
game:GetService("RunService").RenderStepped:Connect(function()
if e==true then
local rcp = RaycastParams.new()
rcp.FilterType=Enum.RaycastFilterType.Blacklist
rcp.FilterDescendantsInstances={game.Players.LocalPlayer.Character,tb}
local ray = workspace:Raycast(workspace.CurrentCamera.CFrame.Position,(mou.Hit.p-workspace.CurrentCamera.CFrame.Position).Unit*500,rcp)
if ray==nil then
tb.Parent=nil
else
local f1 = 0
local f2
print(ray.Normal.Unit)
if math.abs(ray.Normal.Unit.X)>f1 then
f1=math.abs(ray.Normal.Unit.X)
f2 = "x"
end
if math.abs(ray.Normal.Unit.Y)>f1 then
f1=math.abs(ray.Normal.Unit.Y)
f2 = "y"
end
if math.abs(ray.Normal.Unit.Z)>f1 then
f1=math.abs(ray.Normal.Unit.Z)
f2 = "z"
end
local r = 45
local jarsx,jarsy,jarsz
if f2=="y" then
jarsx,jarsy,jarsz = ((CFrame.lookAt(Vector3.new(0,0,0),ray.Normal.Unit,ray.Instance.CFrame:VectorToWorldSpace(Vector3.new(0,1,0)))*CFrame.fromOrientation(math.rad(-90),0,0))*CFrame.fromOrientation(0,math.rad(r),0)):ToOrientation()
else
jarsx,jarsy,jarsz = ((CFrame.lookAt(Vector3.new(0,0,0),ray.Normal.Unit,Vector3.new(0,1,0))*CFrame.fromOrientation(math.rad(-90),0,0))*CFrame.fromOrientation(0,math.rad(r),0)):ToOrientation()
end
tb.Parent=workspace
local i1 = Vector3.new(ray.Position.X,ray.Position.Y,ray.Position.Z)
--if raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal))==Enum.NormalId.Bottom then
-- i1=i1-(tb.PrimaryPart.CFrame.UpVector.Unit*(tb.PrimaryPart.Size.Y/2))
--elseif raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal))==Enum.NormalId.Top then
-- i1=i1+(tb.PrimaryPart.CFrame.UpVector.Unit*(tb.PrimaryPart.Size.Y/2))
--elseif raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal))==Enum.NormalId.Front then
-- i1=i1+(tb.PrimaryPart.CFrame.LookVector.Unit*(tb.PrimaryPart.Size.Z/2))
--elseif raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal))==Enum.NormalId.Back then
-- i1=i1-(tb.PrimaryPart.CFrame.LookVector.Unit*(tb.PrimaryPart.Size.Z/2))
--elseif raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal))==Enum.NormalId.Right then
-- i1=i1+(tb.PrimaryPart.CFrame.RightVector.Unit*(tb.PrimaryPart.Size.X/2))
--elseif raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal))==Enum.NormalId.Left then
-- i1=i1-(tb.PrimaryPart.CFrame.RightVector.Unit*(tb.PrimaryPart.Size.X/2))
--end
local intended = CFrame.new(i1)*CFrame.fromOrientation(jarsx,jarsy,jarsz)
local p1,p2 = boundingbox(tb.Size,CFrame.fromOrientation(jarsx,jarsy,jarsz))
local intended2
print(f2)
print(getSurfaceSize(ray))
local kars = GetWorldOrientedSurface(ray.Instance,raysurfaceface(ray.Normal))
if f2=="y" then
print(math.clamp(kars:pointToObjectSpace(intended.Position).X,-getSurfaceSize(ray).X/2+math.abs(p1.X-p2.X)/2,getSurfaceSize(ray).X/2-math.abs(p1.X-p2.X)/2))
print(kars:pointToObjectSpace(intended.Position).Y)
print(math.clamp(kars:pointToObjectSpace(intended.Position).Z,-getSurfaceSize(ray).Z/2+math.abs(p1.Z-p2.Z)/2,getSurfaceSize(ray).Z/2-math.abs(p1.Z-p2.Z)/2))
intended2 = CFrame.new(Vector3.new(math.clamp(kars:pointToObjectSpace(intended.Position).X,-getSurfaceSize(ray).X/2+math.abs(p1.X-p2.X)/2,getSurfaceSize(ray).X/2-math.abs(p1.X-p2.X)/2),kars:pointToObjectSpace(intended.Position).Y,math.clamp(kars:pointToObjectSpace(intended.Position).Z,-getSurfaceSize(ray).Z/2+math.abs(p1.Z-p2.Z)/2,getSurfaceSize(ray).Z/2-math.abs(p1.Z-p2.Z)/2)))
elseif f2=="x" then
intended2 = CFrame.new(Vector3.new(kars:pointToObjectSpace(intended.Position).X,math.clamp(kars:pointToObjectSpace(intended.Position).Y,-getSurfaceSize(ray).Z/2+math.abs(p1.Y-p2.Y)/2,getSurfaceSize(ray).Z/2-math.abs(p1.Y-p2.Y)/2),math.clamp(kars:pointToObjectSpace(intended.Position).Z,-getSurfaceSize(ray).Z/2+math.abs(p1.Z-p2.Z)/2,getSurfaceSize(ray).Z/2-math.abs(p1.Z-p2.Z)/2)))
elseif f2=="z" then
intended2 = CFrame.new(Vector3.new(math.clamp(kars:pointToObjectSpace(intended.Position).X,-getSurfaceSize(ray).X/2+math.abs(p1.X-p2.X)/2,getSurfaceSize(ray).X/2-math.abs(p1.X-p2.X)/2),math.clamp(kars:pointToObjectSpace(intended.Position).Y,-getSurfaceSize(ray).Z/2+math.abs(p1.Y-p2.Y)/2,getSurfaceSize(ray).Z/2-math.abs(p1.Y-p2.Y)/2),kars:pointToObjectSpace(intended.Position).Z))
end
intended2=CFrame.new(kars:pointToWorldSpace(intended2.Position))
intended2=CFrame.new(intended2.Position)*CFrame.fromOrientation(jarsx,jarsy,jarsz)
game:GetService("TweenService"):Create(tb,TweenInfo.new(.1/3,Enum.EasingStyle.Quad,Enum.EasingDirection.InOut),{CFrame=intended2}):Play()
end
end
end)
Place it in StarterGui
I’m not sure what this script is supposed to be doing, but
-
raysurfaceface
looks like it takes an object-space normal not a world-space normal (which you’re currently doing) so the call toGetWorldOrientedSurface
should look like
GetWorldOrientedSurface(
ray.Instance,
raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal)))
-
GetWorldOrientedSurface
returns two things:a) A
CFrame
representing the center of the surface, aligned with the surface, and
b) AVector3
representing the size of the part (relative to the surface cframe). The X and Y components of this would be the actual dimensions of the surface.
I don’t quite understand what you’re trying to do with the kars
variable and everything, but maybe this helps clear things up.
I don’t quite understand what you’re trying to do with the kars variable and everything, but maybe this helps clear things up.
Yeah, because I’m using the kars variable for these very long lines of code
It’s supposed to be a building script, and right now I’m trying to work on finding the boundaries/clamps for the part on surfaces
GetWorldOrientedSurface( ray.Instance, raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal)))
I did this already, I know how to call the function and stuff, but as you can see I don’t know what to do with its result. I’m needing this for finding the correct world axis orientated CFrame for using :pointToObjectSpace on, so that in an example, instead of (25,10,1) for the position of the part by :pointToObjectSpace’d on, then it’s supposed to be (25,1,10) if it’s on a flat surface that’s facing up but the part itself is rotated (90,0,0), thus the surface facing up is its front surface.
local function raysurfaceface(e) local dotY = e:Dot(Vector3.new(0,1,0)) local dotX = e:Dot(Vector3.new(1,0,0)) local dotZ = e:Dot(Vector3.new(0,0,1)) local surface if (math.abs(dotY) >= 0.9) then if (dotY < 0) then surface = Enum.NormalId.Bottom else surface = Enum.NormalId.Top end elseif (math.abs(dotX) >= 0.9) then if (dotX < 0) then surface = Enum.NormalId.Left else surface = Enum.NormalId.Right end else if (dotZ < 0) then surface = Enum.NormalId.Front else surface = Enum.NormalId.Back end end return surface end
Returns the surface enum for the surface normal of the raycast
Yes, I read that function. I’m saying that the way raysurfaceface
is written, it assumes e
is an object-space normal vector.
However, you’re calling it like this:
This is wrong because ray.Normal
is a world-space normal. I’m suggesting you fix the call site to be
So you’re passing it an object-space normal instead.
I don’t really want to look through all this code to figure out what you’re trying to do right now, but I can explain more about what GetWorldOrientedSurface
returns, if that would be useful.
So I’m having this very weird output here
Corresponds to
local kars = GetWorldOrientedSurface(ray.Instance,raysurfaceface(ray.Instance.CFrame:VectorToObjectSpace(ray.Normal)))
print(kars.LookVector)
print(kars.UpVector)
print(ray.Normal)
I don’t know why the kars lookvector is faced downwards, and also when I print
kars:pointToObjectSpace(intended.Position)
It still returns the usual (x,y,0) instead of (x,0,y) which I was hoping to achieve by finding the world oriented cframe.
And you will see, the reason why I want (x,0,y) is because on a normal top facing surface with rotation (0,0,0), what would matter is the X and Z coordinates, so it would be (clamp(x),y,clamp(z)), but in a top facing surface with rotation (90,0,0), where the top facing surface is actually the part’s front surface, when CFrame:PointToObjectSpace is performed for the ray’s hit coordinates, it would return (x,y,0), instead of (x,0,y), so that’s why the Y coordinate (Z coordinate when using CFrame:PointToWorldSpace) isn’t clamped.
It’s hard to explain, but using rot(90,0,0) size(39,54,2) — kars:PointToObjectSpace() makes the (X,Y,Z) coordinates of the ray hit position into (X,Z,Y), and using rot(90,0,0) size(39,54,2) — kars:PointToWorldSpace() for the converted coordinates will return (X,Z,Y) into (X,Y,Z), and the reason this is problematic is because I’m only clamping the X and Z coordinates for Y facing surfaces, so Y ends up not being clamped in a situation like this, so it becomes (clamp(X),clamp(Y),z) when converted back.
I’ll look at it a little more but just so you’re aware I wrote that function so that the CFrame returned would have its right and upvectors aligned to the surface, while the surface normal is the -LookVector. So it’s expected at least that the look vector would be pointing into the surface
Have you figured out the answer? It’s been an hour already
Have you figured out the solution to this problem
It seems to fail on the top and bottom faces if they’re axis-aligned. Working on it
As well as this problem?
I’ve also drawn a diagram if my words are getting harder to understand
Hey man, I’m volunteering my time like everyone else on this forum.
Anyways, yes.
This version of the function returns a cframe where the RightVector
and -LookVector
are parallel to the surface, and the UpVector
is pointing out from the surface. The second returned value is the size of the surface in a Vector2 this time for clarity.
To be clear: the CFrame this returns has a positive X-axis which points “to the right”, a positive Z-axis which points “down”, and a positive Y-axis which points “out of the page”.
However, the size returned is a Vector2, which means that it’s X-axis is aligned with the CFrame’s X-axis, but the size-X-axis is aligned with the cframe-Z-axis.
Visualization of the CFrame:
-- Returns a CFrame and a Size (Vector2) of the given surface.
--
-- The CFrame:
-- * is positioned at the center of the surface
-- * has an UpVector = the surface normal
-- * has a LookVector aligned with a part edge, and pointing generally upwards in world space.
local function GetWorldOrientedSurface(part: BasePart, normalId: Enum.NormalId): (CFrame, Vector2)
local cf = part.CFrame
local rot = cf - cf.Position
local nObject = Vector3.fromNormalId(normalId)
local nWorld = rot * nObject
-- get orthogonal vector by utilizing the order of NormalId enums
-- i.e. Front.Value is 5 -> (5+1)%6 = 0 -> Right.Value
local xWorld = rot * Vector3.fromNormalId((normalId.Value + 1) % 6)
-- get other orthogonal vector
local zWorld = nWorld:Cross(xWorld)
-- make them both point "generally down"
if xWorld.Y > 0 then xWorld = -xWorld end
if zWorld.Y > 0 then zWorld = -zWorld end
-- choose the one pointing "more down" one as the z axis for the surface
if xWorld.Y < zWorld.Y then zWorld = xWorld end
-- redefine x axis based on that
xWorld = nWorld:Cross(zWorld)
local surfaceRot = CFrame.fromMatrix(Vector3.new(), xWorld, nWorld, zWorld)
-- get width of part in direction of x and y
local sizeInWorldSpace = rot * part.Size
local sizeInSurfaceSpace = surfaceRot:Inverse() * sizeInWorldSpace
-- get position on surface
local surfaceCFrame = surfaceRot + cf.Position + nWorld * math.abs(sizeInSurfaceSpace.Y) / 2
return surfaceCFrame, Vector2.new(math.abs(sizeInSurfaceSpace.X), math.abs(sizeInSurfaceSpace.Z))
end
-- USAGE
local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
local p = Instance.new("Part")
p.CanCollide = false
p.Anchored = true
p.Transparency = 0.5
p.BrickColor = BrickColor.Red()
local decal = Instance.new("Decal")
decal.Texture = "http://www.roblox.com/asset/?id=67616959" -- some random "this side up" image
decal.Face = Enum.NormalId.Top -- always the top face
decal.Parent = p
p.Parent = player.Character or player.CharacterAdded:Wait()
game:GetService("RunService").Stepped:Connect(function()
local part = mouse.Target
if part then
local cframe, size = GetWorldOrientedSurface(part, mouse.TargetSurface)
p.Size = Vector3.new(size.X, 0.1, size.Y)
p.CFrame = cframe
end
end)