Open OOWS opened 1 year ago
Thanks! These are great suggestions. I'll set aside some time to work on an update.
@OOWS here is the proposed PR to address all of your requested improvements https://github.com/FirstVertex/rbxts-scale-model/pull/2/files Would it be possible for you to test these changes, and let me know if this is satisfactory?
Yes, I would be happy to test this. I don't have Typescript, so I would need a way to download the Roblox ModuleScript that has the new code.
here is the entire module you can backup your existing one and overwrite with this
-- Compiled with roblox-ts v1.3.3
--[[
*
* @rbxts/scale-model
*
* USAGE:
* import { scaleModel } '@rbxts/scale-model';
*
* scaleModel(game.Workspace.MyModel, 7, Enum.NormalId.Bottom)
* scalePart(game.Workspace.MyPart, 0.5)
*
]]
local function averageNumbers(numbers)
local count = #numbers
if count == 0 then
return 0
end
local _arg0 = function(acc, cv)
return acc + cv
end
-- ▼ ReadonlyArray.reduce ▼
local _result = 0
local _callback = _arg0
for _i = 1, #numbers do
_result = _callback(_result, numbers[_i], _i - 1, numbers)
end
-- ▲ ReadonlyArray.reduce ▲
return _result / count
end
--[[
*
* A type used to represent the parameters for scaling
]]
--[[
*
* A class used to represent the parameters for scaling
]]
local ScaleSpecifier
do
ScaleSpecifier = setmetatable({}, {
__tostring = function()
return "ScaleSpecifier"
end,
})
ScaleSpecifier.__index = ScaleSpecifier
function ScaleSpecifier.new(...)
local self = setmetatable({}, ScaleSpecifier)
return self:constructor(...) or self
end
function ScaleSpecifier:constructor(_scaleInput)
self._scaleInput = _scaleInput
local inputType = typeof(_scaleInput)
self.isNumber = inputType == "number"
self.isVector2 = inputType == "Vector2"
self.isVector3 = inputType == "Vector3"
self.isScaleSpec = not (self.isNumber or (self.isVector2 or self.isVector3))
if self.isNumber then
local scaleNumber = self._scaleInput
self.asNumber = scaleNumber
self.asVector2 = Vector2.new(scaleNumber, scaleNumber)
self.asVector3 = Vector3.new(scaleNumber, scaleNumber, scaleNumber)
self.asScaleSpec = self
elseif self.isVector2 then
local scaleVec2 = self._scaleInput
self.asNumber = averageNumbers({ scaleVec2.X, scaleVec2.Y })
self.asVector2 = scaleVec2
self.asVector3 = Vector3.new(scaleVec2.X, scaleVec2.Y, self.asNumber)
self.asScaleSpec = self
elseif self.isVector3 then
local scaleVec3 = self._scaleInput
self.asNumber = averageNumbers({ scaleVec3.X, scaleVec3.Y, scaleVec3.Z })
self.asVector2 = Vector2.new(scaleVec3.X, scaleVec3.Y)
self.asVector3 = scaleVec3
self.asScaleSpec = self
elseif self.isScaleSpec then
local scaleSpec = self._scaleInput
self.asNumber = scaleSpec.asNumber
self.asVector2 = scaleSpec.asVector2
self.asVector3 = scaleSpec.asVector3
self.asScaleSpec = scaleSpec
end
end
end
--[[
*
* Scale a Model and all descendants uniformly
* @param model The Model to scale
* @param scale The amount to scale. > 1 is bigger, < 1 is smaller
* @param center (Optional) The point about which to scale. Default: the Model's PivotPoint's Position
]]
local _centerToOrigin, scaleDescendants
local function scaleModel(model, scale, center)
if scale == 1 then
return nil
end
local origin
if center and typeof(center) == "Vector3" then
origin = center
else
origin = _centerToOrigin(center, model:GetExtentsSize(), model:GetPivot())
end
scaleDescendants(model, scale, origin)
end
--[[
*
* Scale a Part and all descendants uniformly
* @param part The Part to scale
* @param scale The amount to scale. > 1 is bigger, < 1 is smaller
* @param center (Optional) The point about which to scale. Default: the Part's Position
]]
local _scaleBasePart
local function scalePart(part, scale, center)
if scale == 1 then
return nil
end
local origin = _centerToOrigin(center, part.Size, part:GetPivot())
_scaleBasePart(part, scale, origin)
scaleDescendants(part, scale, origin)
end
local function lerpVector(vector, center, sspec)
local delta = vector - center
-- const {X, Y, Z} = vector;
local centerX = center.X
local centerY = center.Y
local centerZ = center.Z
local scaleVec = sspec.asVector3
local scaleX = scaleVec.X
local scaleY = scaleVec.Y
local scaleZ = scaleVec.Z
return Vector3.new(centerX + (delta.X * scaleX), centerY + (delta.Y * scaleY), centerZ + (delta.Z * scaleZ))
end
--[[
*
* Scale a Vector uniformly
* @param vector The Vector to scale
* @param scale The amount to scale. > 1 is bigger, < 1 is smaller
* @param center (Optional) The point about which to scale. Default: position not considered
]]
local function scaleVector(vector, scale, center)
if scale == 1 then
return vector
end
local sspec = ScaleSpecifier.new(scale)
if center then
return lerpVector(vector, center, sspec)
else
local _result
if sspec.isVector3 then
local _asVector3 = sspec.asVector3
_result = vector * _asVector3
else
local _asNumber = sspec.asNumber
_result = vector * _asNumber
end
return _result
end
end
--[[
*
* Scale an Explosion uniformly
* @param explosion The Explosion to scale
* @param scale The amount to scale. > 1 is bigger, < 1 is smaller
]]
local function scaleExplosion(explosion, scale)
if scale == 1 then
return nil
end
local sspec = ScaleSpecifier.new(scale)
local _result
if sspec.isVector3 then
local _position = explosion.Position
local _asVector3 = sspec.asVector3
_result = _position * _asVector3
else
local _position = explosion.Position
local _asNumber = sspec.asNumber
_result = _position * _asNumber
end
explosion.Position = _result
explosion.BlastPressure *= sspec.asNumber
explosion.BlastRadius *= sspec.asNumber
end
--[[
*
* Scale a Tool uniformly
* @param tool The Tool to scale
* @param scale The amount to scale. > 1 is bigger, < 1 is smaller
* @param center (Optional) The point about which to scale. Default: the Tool's Handle's Position
]]
local function scaleTool(tool, scale, center)
if scale == 1 then
return nil
end
local origin
if center and typeof(center) == "Vector3" then
origin = center
else
local handle = tool:FindFirstChild("Handle")
if not handle then
print("Unable to scale tool, no center nor Handle has been defined")
return nil
end
origin = _centerToOrigin(center, handle.Size, handle:GetPivot())
end
scaleDescendants(tool, scale, origin)
end
--[[
*
* Scale a Humanoid uniformly
* @param humanoid The Humanoid to scale
* @param scale The amount to scale. > 1 is bigger, < 1 is smaller
]]
local function scaleHumanoid(humanoid, scale, center)
if humanoid.RigType == Enum.HumanoidRigType.R15 then
local sNumber = ScaleSpecifier.new(scale).asNumber
local scales = {
head = humanoid:FindFirstChild("HeadScale"),
bodyDepth = humanoid:FindFirstChild("BodyDepthScale"),
bodyWidth = humanoid:FindFirstChild("BodyWidthScale"),
bodyHeight = humanoid:FindFirstChild("BodyHeightScale"),
}
if scales.head then
scales.head.Value *= sNumber
end
if scales.bodyDepth then
scales.bodyDepth.Value *= sNumber
end
if scales.bodyWidth then
scales.bodyWidth.Value *= sNumber
end
if scales.bodyHeight then
scales.bodyHeight.Value *= sNumber
end
else
local char = humanoid.Parent
if char then
return scaleModel(char, scale, center)
end
end
end
local function disableWelds(container)
local welds = {}
local desc = container:GetDescendants()
for _, instance in ipairs(desc) do
if instance:IsA("WeldConstraint") then
local _enabled = instance.Enabled
welds[instance] = _enabled
instance.Enabled = false
end
end
return welds
end
local function enableWelds(welds)
local _arg0 = function(value, wc)
wc.Enabled = value
end
for _k, _v in pairs(welds) do
_arg0(_v, _k, welds)
end
end
-- function snapshotChildRelationships(container: Instance, origin: CFrame): Map<BasePart, CFrame> {
-- const childRels = new Map<BasePart, CFrame>();
-- const desc = container.GetDescendants();
-- for (const instance of desc) {
-- if (instance.IsA("BasePart")) {
-- childRels.set(instance, origin.ToObjectSpace(instance.CFrame));
-- }
-- }
-- return childRels;
-- }
-- function restoreChildRelationships(childRels: Map<BasePart, CFrame>, origin: CFrame, scale: ScaleInputType) {
-- childRels.forEach((cf: CFrame, bp: BasePart) => {
-- const orientation = cf.sub(cf.Position);
-- const pos = origin.ToWorldSpace(new CFrame(scaleVector(cf.Position, scale)));
-- bp.CFrame = pos.mul(orientation);
-- });
-- }
--[[
*
* Scale an array of Instances uniformly
* @param instance The Instance to scale
* @param scale The amount to scale. > 1 is bigger, < 1 is smaller
]]
local _scaleAttachment, scaleMesh, scaleFire, scaleParticle, scaleTexture, scalePointLight, scaleJoint
local function scaleInstance(instance, scale, origin)
if scale == 1 then
return nil
end
local scaledChildren = false
if instance:IsA("BasePart") then
scalePart(instance, scale, origin)
scaledChildren = true
elseif instance:IsA("Model") then
scaleModel(instance, scale, origin)
scaledChildren = true
elseif instance:IsA("Attachment") then
_scaleAttachment(instance, scale, origin)
elseif instance:IsA("Tool") then
scaleTool(instance, scale, origin)
scaledChildren = true
elseif instance:IsA("SpecialMesh") then
scaleMesh(instance, scale, origin)
elseif instance:IsA("Fire") then
scaleFire(instance, scale, origin)
elseif instance:IsA("Explosion") then
scaleExplosion(instance, scale)
elseif instance:IsA("ParticleEmitter") then
scaleParticle(instance, scale)
elseif instance:IsA("Texture") then
scaleTexture(instance, scale, origin)
elseif instance:IsA("PointLight") then
scalePointLight(instance, scale)
elseif instance:IsA("JointInstance") then
scaleJoint(instance, scale)
end
if not scaledChildren then
scaleDescendants(instance, scale, origin, true)
end
end
--[[
*
* Scale an array of Instances uniformly
* @param container The parent of the Instances to scale
* @param scale The amount to scale. > 1 is bigger, < 1 is smaller
]]
function scaleDescendants(container, scale, origin, recur)
if recur == nil then
recur = false
end
if scale == 1 then
return nil
end
local welds = if recur then nil else disableWelds(container)
local instances = container:GetChildren()
for _, instance in ipairs(instances) do
scaleInstance(instance, scale, origin)
end
if welds then
enableWelds(welds)
end
end
function scaleTexture(texture, scale, origin)
local sspecV2 = ScaleSpecifier.new(scale).asVector2
texture.OffsetStudsU *= sspecV2.X
texture.OffsetStudsV *= sspecV2.Y
texture.StudsPerTileU *= sspecV2.X
texture.StudsPerTileV *= sspecV2.Y
end
function scaleMesh(mesh, scale, _origin)
local _scale = mesh.Scale
local _asNumber = ScaleSpecifier.new(scale).asNumber
mesh.Scale = _scale * _asNumber
end
function scaleFire(fire, scale, _origin)
fire.Size = math.floor(fire.Size * ScaleSpecifier.new(scale).asNumber)
end
local scaleNumberSequence
function scaleParticle(particle, scale)
particle.Size = scaleNumberSequence(particle.Size, scale)
end
function scaleNumberSequence(sequence, scale)
local scaleNum = ScaleSpecifier.new(scale).asNumber
local _keypoints = sequence.Keypoints
local _arg0 = function(kp)
return NumberSequenceKeypoint.new(kp.Time, kp.Value * scaleNum, kp.Envelope * scaleNum)
end
-- ▼ ReadonlyArray.map ▼
local _newValue = table.create(#_keypoints)
for _k, _v in ipairs(_keypoints) do
_newValue[_k] = _arg0(_v, _k - 1, _keypoints)
end
-- ▲ ReadonlyArray.map ▲
return NumberSequence.new(_newValue)
end
function scalePointLight(light, scale)
local scaleNum = ScaleSpecifier.new(scale).asNumber
light.Range *= scaleNum
end
function scaleJoint(joint, scale)
local c0NewPos = scaleVector(joint.C0.Position, scale)
local _c0 = joint.C0
local _position = joint.C0.Position
local c0Rot = _c0 - _position
local c1NewPos = scaleVector(joint.C1.Position, scale)
local _c1 = joint.C1
local _position_1 = joint.C1.Position
local c1Rot = _c1 - _position_1
joint.C0 = CFrame.new(c0NewPos) * c0Rot
joint.C1 = CFrame.new(c1NewPos) * c1Rot
end
local _minSide
function _centerToOrigin(center, size, pivot)
local origin
if typeof(center) == "Vector3" then
origin = center
else
if center then
origin = _minSide(size, pivot.Position, center)
else
origin = pivot.Position
end
end
return origin
end
function _minSide(size, position, side)
local halfSize = size * 0.5
repeat
if side == (Enum.NormalId.Front) then
return Vector3.new(position.X, position.Y, position.Z - halfSize.Z)
end
if side == (Enum.NormalId.Back) then
return Vector3.new(position.X, position.Y, position.Z + halfSize.Z)
end
if side == (Enum.NormalId.Right) then
return Vector3.new(position.X + halfSize.X, position.Y, position.Z)
end
if side == (Enum.NormalId.Left) then
return Vector3.new(position.X - halfSize.X, position.Y, position.Z)
end
if side == (Enum.NormalId.Top) then
return Vector3.new(position.X, position.Y + halfSize.Y, position.Z)
end
if side == (Enum.NormalId.Bottom) then
return Vector3.new(position.X, position.Y - halfSize.Y, position.Z)
end
until true
return position
end
function _scaleBasePart(part, scale, origin)
local _cFrame = part.CFrame
local _position = part.Position
local angle = _cFrame - _position
local sspec = ScaleSpecifier.new(scale)
local pos = lerpVector(part.Position, origin, sspec)
local _result
if sspec.isVector3 then
local _size = part.Size
local _asVector3 = sspec.asVector3
_result = _size * _asVector3
else
local _size = part.Size
local _asNumber = sspec.asNumber
_result = _size * _asNumber
end
part.Size = _result
part.CFrame = CFrame.new(pos) * angle
end
function _scaleAttachment(attachment, scale, _origin)
local parent = attachment:FindFirstAncestorWhichIsA("BasePart")
if parent then
attachment.WorldPosition = lerpVector(attachment.WorldPosition, parent.Position, ScaleSpecifier.new(scale))
end
end
return {
scaleModel = scaleModel,
scalePart = scalePart,
scaleVector = scaleVector,
scaleExplosion = scaleExplosion,
scaleTool = scaleTool,
scaleHumanoid = scaleHumanoid,
scaleInstance = scaleInstance,
scaleDescendants = scaleDescendants,
scaleTexture = scaleTexture,
scaleMesh = scaleMesh,
scaleFire = scaleFire,
scaleParticle = scaleParticle,
scaleNumberSequence = scaleNumberSequence,
scalePointLight = scalePointLight,
scaleJoint = scaleJoint,
ScaleSpecifier = ScaleSpecifier,
}
I did some testing, and there are just a couple of issues.
Humanoid For some reason the humanoid model gets moved to a very distant position when scaled. I did some experimenting, and if you anchor all the parts prior to scaling, then scale, and then unanchor the parts - then everything works fine. Not sure why this is, since if I scale using the humanoid properties directly in Studio, the model scales fine without moving.
When pivot point is not directly in the center. If the pivot point is not in the center of the basepart or model, then when I scale up and then scale back down, the position of the basepart or model has moved. For example, with a basepart that has the pivot point on the bottom, I scale up, then scale back down - then the position of the basepart has moved upward from its original position. I believe the offset is the distance from the basepart midpoint to the pivot point. So somewhere in the code, this position probably uses the midpoint instead of the pivot point.
Typo In the readme, you reference
scaleInstances()
, but in the code this is actuallyscaleDescendants()
.Pivot Point Support It would be very helpful if you could add support for pivot points, so that the scaling occurs relative to the pivot point. This is also helpful since the model no longer requires a PrimaryPart, since you can just use the new GetPivot() function. Keep in mind, that you would also need to scale the Pivot Point position offset as explained here:
https://devforum.roblox.com/t/how-to-scale-the-pivot-point-of-a-model/1826773/2
Humanoid Scaling Please add support for scaling humanoids, which is easily done through the humanoid scaling properties.
Scale Single Instance It would be convenient to have a new function which scales a single instance. Similar to
scaleDescendants()
, but just for a single instance. That saves me from having to use an if-then statement to find the proper scaling method.Thank you.