I am trying to position parts at the extents of a given part, the parts must never be inside of the main part. A good use case would be positioning gems or ore onto a rock… Here is what I have right now - however this solution only guarantees random positioning on the Y axis, as I cannot figure out how to guarantee on all three without ending up on the inside.

Is the shape of the rock always a rectangular prism? Or is it a mesh/combination of parts, or do you want to position the gems on a sphere around the rock?

The shape of the rock, currently, is always a rectangular prism, as I figured it will be must easier to calculate this type of generation on a constant shape… Not sure how I’d handle odd mesh parts without multiple rectangular ‘nodes’ to spawn the gems onto.

At this time, I’d just like to position the gems on a rectangular/square part, at a random position on the extents of the part.

create a new vector3 with either its x, y, or z value = to +/- bounds x, y, or z respectively. Ex: create a vector with the y component the -y component of the bounds vector. The other two components can be any random numbers between their respective +/- bound components

call rock.CFrame:positionToWorldSpace() on the new vector and you have your gem position!

Put this together quickly for you. Note that a generated point is equally likely to be on each face, so it is not a uniform distribution over the surface area.

local RNG = Random.new()
local function RandomPointOnPart(Part)
local c = Part.CFrame
local s = Part.Size * 0.5
local sx, sy, sz = s.x, s.y, s.z
local x = RNG:NextNumber(-sx, sx)
local y = RNG:NextNumber(-sy, sy)
local z = RNG:NextNumber(-sz, sz)
local f = RNG:NextInteger(1, 6)
if f == 1 then -- clamp +x
return c * Vector3.new(sx, y, z)
elseif f == 2 then -- clamp -x
return c * Vector3.new(-sx, y, z)
elseif f == 3 then -- clamp +y
return c * Vector3.new(x, sy, z)
elseif f == 4 then -- clamp -y
return c * Vector3.new(x, -sy, z)
elseif f == 5 then -- clamp +z
return c * Vector3.new(x, y, sz)
elseif f == 6 then -- clamp -z
return c * Vector3.new(x, y, -sz)
end
end

-- Beware! Untested code ahead!
local components = {'X', 'Y', 'Z'}
local signs = {1, -1}
local function getGemPos(rock)
local bounds = rock.Size/2
local vector = {}
local component = components[math.random(1, 3)]
local sign = signs[math.random(1, 2)]
vector[component] = sign * bounds[component]
for i, component in ipairs(components) do
if not vector[component] then
vector[component] = math.random() * bounds[component] * 2 - bounds[component]
end
end
return rock.CFrame:positionToWorldSpace(Vector3.new(vector.X, vector.Y, vector.Z))
end

this worked great! If i passed a BasePart instance (the gem) as the second parameter, which line would I add the +/- size/2 to center the gem on the surface?

edit: never mind, I just realized how simple of an answer that is

While not very elegant, there is a solution if you wanted to support attaching to any kind of object. You could make a random vector, multiply it by the parts size plus a bit extra, add this vector to the parts position then raycast back to the part.

I decided to clean it up a bit… I like this version a lot more. I took some of what @sircfenner did too. It is down to only 12 lines!

local components = { 'X', 'Y', 'Z' }
local signs = { 0.5, -0.5 }
local RNG = math.random
local function getPosOnPart(part)
local vector = {}
for _, component in pairs(components) do
vector[component] = RNG() - 0.5
end
vector[components[RNG(1, 3)]] = signs[RNG(1, 2)]
return part.CFrame * part.Size * Vector3.new(vector.X, vector.Y, vector.Z)
end

I ended up switching over to MeshParts, as I know I will end up doing that in the long run. I am using a raycasting method to get a random surface point of the mesh.

I am using a variation of @IdiomicLanguage’s orignal response (before edit) as a backup, in case a raycast ever fails (shouldn’t happen)

For anyone who may be curious, this is my end product:

You can actually simplify this a bit, by simply getting a random point on the surface of a sphere centered at the part’s position, and then raycasting inwards from that point.