Need help with an improved Voxel Editor

…man is it hard to select and copy things on the forums these days!

The last thing I think the editor needs is a nudge function.

I need a function to bump the entire Voxel model one block in any direction, with the wrinkle that if the model goes past a grid boundary it wraps around to the other side (demonstration image attached).

Can anyone (@dave1707 maybe? :wink: ) help me with a function that does this?

@UberGoober Just use different for loops depending on the direction you want to move. If a move exceeds the limit of the move, move it to the other side. You just need to know the beginning x,y,z positions of each block, add or subtract 1 from x or y or z to reposition the block in the direction you want.

@dave1707 I get the concepts involved, I just don’t know how to actually do it.

I know I could fumble and stumble and figure it out on my own, but the whole reason this thread’s title is a plea for help is so hopefully we can pool our expertise to more quickly get this thing in shape to replace the official Voxel Editor.

So if you wouldn’t mind helping out with some sample code on how to do this I’d really appreciate it.

@UberGoober Heres a quick example. Move the parameters to select a movement then tap move.

PS. I only added code to wrap the y value. Set the y parameter to +1 and keep tapping move. Eventually it will wrap.

viewer.mode=STANDARD

function setup()
    parameter.integer("xm",-1,1,0)
    parameter.integer("ym",-1,1,0)
    parameter.integer("zm",-1,1,0)
    parameter.action("move",move)
    tab={}
    fill(255)
    assert(OrbitViewer, "Please include Cameras as a dependency")
    scene = craft.scene()
    skyMaterial=scene.sky.material
    skyMaterial.sky=color(0, 62, 255, 255)
    skyMaterial.horizon=color(99, 255, 0, 255)
    
    scene.sun.rotation=quat.eulerAngles(20,45,-30)
    v=scene.camera:add(OrbitViewer, vec3(0,0,0), 100, 0, 200)
    v.rx,v.ry=30,30
    map = readImage(asset.builtin.Surfaces.Basic_Bricks_AO)
    
    -- create some random cubes to move
    for x=1,10 do
        for y=1,10 do
            for z=1,10 do   
                if math.random(10)<3 then  
                    createRect(x,y,z)        
                end
            end
        end
    end
end

function draw()
    update(DeltaTime)
    scene:draw()
    text("make choice then tap move parameter",WIDTH/2,HEIGHT-100) 
end

function update(dt)
    scene:update(dt)
end

function createRect(x,y,z) -- create cubes at random positions
    local c1 = scene:entity()
    c1.position=vec3(x,y,z)
    c1.model = craft.model.cube(vec3(1,1,1))
    c1.material = craft.material(asset.builtin.Materials.Specular)
    c1.material.map = map
    table.insert(tab,c1)
end

function move() -- move in selected direction    
    for a,b in pairs(tab) do
        b.position=b.position+vec3(xm,ym,zm)
        
        -- only code for the y value wraps the cubes
        if b.position.y>20 then
            p=b.position
            p.y=1
            b.position=p
        end
    end
end

@dave1707 the problem is that Voxel Editor isn’t creating entities, it’s creating Voxel blocks, and also that it doesn’t keep a separate table of every block.

This is my attempt to move just the y value upward for every block in the volume:


function move() -- move in selected direction 
    local sizeX, sizeY, sizeZ = G.volume:size()
    for z = 0, sizeZ - 1 do
        for y = 0, sizeY - 1 do
            for x = 0, sizeX - 1 do
                local id = G.volume:get(vec3(x, y, z), BLOCK_ID)
                local newVec = vec3(x, y, z) + vec3(0, 1, 0)
                if newVec.y > sizeY - 1 then
                    newVec.y = 1
                end
                G.volume:set(newVec, BLOCK_ID)
            end
        end
    end
end

…and the result of this code is that all blocks disappear except the ones with y values of 0. So I’m doing something very wrong here.

@dave1707

So, this works, but it’s only for up and down, so to cover all directions would require repeating all this code two more times, and it seems like there has to be a better way:


function colorFromInt(int)
    local r = (int>>24) & 255
    local g = (int>>16) & 255   
    local b = (int>>8) & 255     
    return color(r, g, b)
end

function move(up)
    local sizeX, sizeY, sizeZ = volume:size()
    local volumeTable = {}
    if not up then
        for z = 0, sizeZ - 1 do
            for y = 0, sizeY - 1 do
                for x = 0, sizeX - 1 do
                    local blockTable
                    local id = volume:get(vec3(x, y, z), BLOCK_ID)
                    local newVec = vec3(x, y, z) - vec3(0, 1, 0)
                    if y == 0 then
                        newVec.y = sizeY - 1
                    end
                    local colorInt = volume:get(x, y, z, BLOCK_STATE)
                    if colorInt ~= 0 then
                        blockTable = {newVec, BLOCK_ID, id, "color", colorFromInt(colorInt)}
                    else 
                        blockTable = {newVec, BLOCK_ID, id}
                    end
                    table.insert(volumeTable, blockTable)
                end
            end
        end
    else
        for z = sizeZ - 1, 0, -1 do
            for y = sizeY - 1, 0, -1 do
                for x = sizeX - 1, 0, -1 do
                    local blockTable
                    local id = volume:get(vec3(x, y, z), BLOCK_ID)
                    local newVec = vec3(x, y, z) + vec3(0, 1, 0)
                    if newVec.y == sizeY then
                        newVec.y = 0
                    end
                    local colorInt = volume:get(x, y, z, BLOCK_STATE)
                    if colorInt then
                        blockTable = {newVec, BLOCK_ID, id, "color", colorFromInt(colorInt)}
                    else 
                        blockTable = {newVec, BLOCK_ID, id}
                    end
                    table.insert(volumeTable, blockTable)
                end
            end
        end
    end
    for i, blockTable in ipairs(volumeTable) do
        volume:set(table.unpack(blockTable))
    end
end

If you want to see it in action, a version of the editor that implements it is attached; just use the “nudge” and “nudge up” buttons at the top of the overlay panel.

@UberGoober Is this closer.

viewer.mode=STANDARD

function setup()
    parameter.action("X+", function() move(1,0,0) end )
    parameter.action("X-", function() move(-1,0,0) end )
    parameter.action("Y+", function() move(0,1,0) end )
    parameter.action("Y-", function() move(0,-1,0) end )
    parameter.action("Z+", function() move(0,0,1) end )
    parameter.action("Z-", function() move(0,0,-1) end )
    assert(OrbitViewer, "Please include Cameras as a dependency")
    scene = craft.scene() 
    v=scene.camera:add(OrbitViewer,vec3(5,5,0), 100, 0, 2000)
    v.rx=30
    v.ry=30
    scene.voxels.blocks:addAssetPack("Blocks")
    snow = scene.voxels.blocks:new("Snow")
    snow.setTexture(ALL, "Blocks:Snow")
    
    scene.voxels:resize(vec3(10,1,10))          
    scene.voxels.coordinates = vec3(0,0,0) 
   
    -- create a random voxel group 
    scene.voxels:fill("Snow")
    for x=3,10 do
        for y=3,10 do
            for z=3,10 do
                if math.random(10)>9 then
                    scene.voxels:block(x,y,z)
                end
            end
        end
    end
end

function update(dt)
    scene:update(dt)
end

function draw()
    update(DeltaTime)
    scene:draw()
end

function xx()   
    -- read the voxel area and save any snow-ID x,y,z positions
    -- clear the blocks with EMPTY
    tab={}
    for x=1,15 do
        for y=1,15 do
            for z=1,15 do
                -- get the name of the voxel at x,y,z
                name=scene.voxels:get(vec3(x,y,z),BLOCK_NAME)
                if name=="Snow" then
                    -- putx,y,z values in a table for later
                    table.insert(tab,vec3(x,y,z))
                    -- set voxel block to EMPTY
                    scene.voxels:fill(EMPTY)
                    scene.voxels:block(x,y,z)
                end
            end
        end
    end 
end

function move(x,y,z)  
    xx() 
    for a,b in pairs(tab) do 
        scene.voxels:fill("Snow")
        if b.x+x>15 then
            scene.voxels:block(1,b.y,b.z)
        elseif b.y+y>15 then
            scene.voxels:block(b.x+x,1,b.z)
        elseif b.z+z>15 then
            scene.voxels:block(b.x+x,b.y,1)
        elseif b.x+x<1 then
            scene.voxels:block(15,b.y,b.z)
        elseif b.y+y<1 then
            scene.voxels:block(b.x+x,15,b.z)
        elseif b.z+z<1 then
            scene.voxels:block(b.x+x,b.y,15)
        else
            scene.voxels:block(b.x+x,b.y+y,b.z+z)
        end
    end
end

@dave1707 that’s great!

I tweaked it to move any kind of block:


viewer.mode=STANDARD

function setup()
    assert(OrbitViewer, "Please include Cameras as a dependency")
    scene = craft.scene()
    skyMaterial=scene.sky.material
    skyMaterial.sky=color(163, 178, 223)
    skyMaterial.horizon=color(195, 224, 177)
    scene.sun.rotation=quat.eulerAngles(20,45,-30)
    v=scene.camera:add(OrbitViewer, vec3(5, 8, 0), 10, 0, 200)
    --v.rx,v.ry=30,30
    map = readImage(asset.builtin.Blocks.Redstone_Emerald_Alt)
    
    nudgeTimer = 0
    parameter.integer("nudgeX", -1, 1, 0)
    parameter.integer("nudgeY", -1, 1, 0)
    parameter.integer("nudgeZ", -1, 1, 0)

    scene.voxels.blocks:addAssetPack("Blocks")
    snow = scene.voxels.blocks:new("Snow")
    snow.setTexture(ALL, "Blocks:Stone Gold")
    
    scene.voxels:resize(vec3(10,1,10))          
    scene.voxels.coordinates = vec3(0,0,0) 
    
    -- create a random voxel group 
    scene.voxels:fill("Snow")
    for x=3,10 do
        for y=3,10 do
            for z=3,10 do
                if math.random(10)>9 then
                    scene.voxels:block(x,y,z)
                end
            end
        end
    end
end

function nudgeCheck()
    if nudgeTimer == 0 then
        if nudgeX ~= 0 then
            move(nudgeX, 0, 0)
            nudgeTimer = ElapsedTime
        end
        if nudgeY ~= 0 then
            move(0, nudgeY, 0)
            nudgeTimer = ElapsedTime
        end
        if nudgeZ ~= 0 then
            move(0, 0, nudgeZ)
            nudgeTimer = ElapsedTime
        end
    elseif ElapsedTime - nudgeTimer > 0.25 then
        nudgeX, nudgeY, nudgeZ = 0, 0, 0
        nudgeTimer = 0
    end
end

function update(dt)
    scene:update(dt)
    nudgeCheck()
end

function draw()
    update(DeltaTime)
    scene:draw()
end

function xx()   
    -- read the voxel area and save any snow-ID x,y,z positions
    -- clear the blocks with EMPTY
    tab={}
    for x=1,15 do
        for y=1,15 do
            for z=1,15 do
                -- get the name of the voxel at x,y,z
                name=scene.voxels:get(vec3(x,y,z),BLOCK_NAME)
                    -- put x,y,z and name in a table for later
                    table.insert(tab,{pos=vec3(x,y,z), name = name})
                    -- set voxel block to EMPTY
                    scene.voxels:fill(EMPTY)
                    scene.voxels:block(x,y,z)
            end
        end
    end 
end

function move(x,y,z)  
    xx() 
    for a,b in pairs(tab) do 
        scene.voxels:fill(b.name)
        b = b.pos
        if b.x+x>15 then
            scene.voxels:block(1,b.y,b.z)
        elseif b.y+y>15 then
            scene.voxels:block(b.x+x,1,b.z)
        elseif b.z+z>15 then
            scene.voxels:block(b.x+x,b.y,1)
        elseif b.x+x<1 then
            scene.voxels:block(15,b.y,b.z)
        elseif b.y+y<1 then
            scene.voxels:block(b.x+x,15,b.z)
        elseif b.z+z<1 then
            scene.voxels:block(b.x+x,b.y,15)
        else
            scene.voxels:block(b.x+x,b.y+y,b.z+z)
        end
    end
end

This version can move colored blocks, which will be the actual application of this code.

Apparently using get with COLOR doesn’t work, which is why the colorFromInt function is needed.


viewer.mode=OVERLAY

function setup()
    assert(OrbitViewer, "Please include Cameras as a dependency")
    scene = craft.scene()
    skyMaterial=scene.sky.material
    skyMaterial.sky=color(163, 178, 223)
    skyMaterial.horizon=color(195, 224, 177)
    scene.sun.rotation=quat.eulerAngles(20,45,-30)
    v=scene.camera:add(OrbitViewer, vec3(5, 8, 0), 10, 0, 200)
    --v.rx,v.ry=30,30
    map = readImage(asset.builtin.Blocks.Redstone_Emerald_Alt)
    
    nudgeTimer = 0
    parameter.integer("nudgeX", -1, 1, 0)
    parameter.integer("nudgeY", -1, 1, 0)
    parameter.integer("nudgeZ", -1, 1, 0)
    
    scene.voxels:resize(vec3(10,1,10))          
    scene.voxels.coordinates = vec3(0,0,0) 
    
    volume = scene:entity():add(craft.volume, 15, 15, 15)
    
    -- create a random voxel group with random colors
    for x=3,10 do
        for y=3,10 do
            for z=3,10 do
                if math.random(10)>9 then
                    local randColor = color(math.random(255), math.random(255), math.random(255))
                    volume:set(x,y,z,COLOR,randColor,BLOCK_NAME,"Solid")
                end
            end
        end
    end
end

function nudgeCheck()
    if nudgeTimer == 0 then
        if nudgeX ~= 0 then
            move(nudgeX, 0, 0)
            nudgeTimer = ElapsedTime
        end
        if nudgeY ~= 0 then
            move(0, nudgeY, 0)
            nudgeTimer = ElapsedTime
        end
        if nudgeZ ~= 0 then
            move(0, 0, nudgeZ)
            nudgeTimer = ElapsedTime
        end
    elseif ElapsedTime - nudgeTimer > 0.25 then
        nudgeX, nudgeY, nudgeZ = 0, 0, 0
        nudgeTimer = 0
    end
end

function update(dt)
    scene:update(dt)
    nudgeCheck()
end

function draw()
    update(DeltaTime)
    scene:draw()
end

function colorFromInt(int)
    local r = (int>>24) & 255
    local g = (int>>16) & 255   
    local b = (int>>8) & 255     
    return color(r, g, b)
end

function extractBlockData()   
    -- read the voxel area
    -- save block data
    -- clear blocks with EMPTY
    tab={}
    local vX, vY, vZ = volume:size()
    vX, vY, vZ = vX - 1, vY - 1, vZ -1
    for x=0, vX do
        for y=0, vY do
            for z=0, vZ do
                -- get the name of the voxel at x,y,z
                local name = volume:get(x,y,z,BLOCK_ID)
                local pos = vec3(x, y, z)
                local colorInt =  volume:get(x, y, z, BLOCK_STATE)
                local blockTable = {vec3(x,y,z), BLOCK_ID, name}
                if colorInt ~= 0 then
                    table.insert(blockTable, COLOR)
                    table.insert(blockTable, colorFromInt(colorInt))
                end
                -- put x,y,z and name in a table for later
                table.insert(tab,blockTable)
                -- set voxel block to EMPTY
                volume:set(x,y,z,BLOCK_NAME,"Empty")
            end
        end
    end 
end

function move(x,y,z)  
    extractBlockData() 
    local vX, vY, vZ = volume:size()
    vX, vY, vZ = vX - 1, vY - 1, vZ -1
    for a,b in pairs(tab) do 
        local pos = table.remove(b, 1)
        if pos.x+x>vX then
            pos = vec3(0,pos.y,pos.z)
        elseif pos.y+y>vY then
            pos = vec3(pos.x+x,0,pos.z)
        elseif pos.z+z>vZ then
            pos = vec3(pos.x+x,pos.y,0)
        elseif pos.x+x<0 then
            pos = vec3(vX, pos.y, pos.z)
        elseif pos.y+y<0 then
            pos = vec3(pos.x+x,vY,pos.z)
        elseif pos.z+z<0 then
            pos = vec3(pos.x+x,pos.y,vZ)
        else
            pos = vec3(pos.x+x,pos.y+y,pos.z+z)
        end
        volume:set(pos,table.unpack(b))
    end
end

Update:

Nudge is in!

Code still messy. :slight_smile:

Update: fixed bug where nudged models would reset when edited

Question:

Should I include some kind of user avatar for the movement mode?

I was thinking maybe a tiny helicopter, since the icon for movement mode is a helicopter?

It would disappear during editing mode so it wouldn’t block anything.

Yes/no?

A helicopter does sound cute!

Update

Mirroring got broken somewhere along the way. Fixed now.

Update

Mirroring was broken a whole different way and that’s now fixed too.

Update

RGB sliders are now HSV sliders, much easier to get an exact color with.

What do you think of this helicopter?

https://youtu.be/uzar1-E8DdI

It’s supposed to look like the copter in the iOS emoji (as seen in the button in the upper right.)

Also what do you think of the new grid color?

Wow, looking great. Not sure about purple for the grid, was that to make it stand out more?

@John I just kind of did the purple accidentally and thought it looked good. But it’s such a strong choice that it might wear out its welcome…

@John I would love it if the grid didn’t fade out with distance but I can’t seem to find where that’s set. Can it be changed? It’s the same grid code as in your original Voxel Editor.