FirstVertex / rbxts-scale-model

Scale Models and Parts and all Descendants Uniformly
MIT License
5 stars 1 forks source link

Suggested Improvements #1

Open OOWS opened 1 year ago

OOWS commented 1 year ago

Typo In the readme, you reference scaleInstances(), but in the code this is actually scaleDescendants().

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.

FirstVertex commented 1 year ago

Thanks! These are great suggestions. I'll set aside some time to work on an update.

FirstVertex commented 1 year ago

@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?

OOWS commented 1 year ago

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.

FirstVertex commented 1 year ago

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,
}
OOWS commented 1 year ago

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.