162 lines
6.8 KiB
Python
162 lines
6.8 KiB
Python
"""
|
|
Import a small image (like a nintendo sprite) and create it out of cube prims
|
|
|
|
Inefficient and doesn't even do line fill, expect it to take `width * height`
|
|
prims for whatever image you import!
|
|
"""
|
|
|
|
import asyncio
|
|
import struct
|
|
from typing import *
|
|
|
|
from PySide6.QtGui import QImage
|
|
|
|
from hippolyzer.lib.base.datatypes import UUID, Vector3, Quaternion
|
|
from hippolyzer.lib.base.helpers import to_chunks
|
|
from hippolyzer.lib.base.message.message import Block, Message
|
|
from hippolyzer.lib.base.templates import ObjectUpdateFlags, PCode, MCode, MultipleObjectUpdateFlags, \
|
|
TextureEntryCollection, JUST_CREATED_FLAGS
|
|
from hippolyzer.lib.client.object_manager import ObjectEvent, ObjectUpdateType
|
|
from hippolyzer.lib.proxy.addon_utils import BaseAddon
|
|
from hippolyzer.lib.proxy.addons import AddonManager
|
|
from hippolyzer.lib.proxy.commands import handle_command
|
|
from hippolyzer.lib.base.network.transport import Direction
|
|
from hippolyzer.lib.proxy.region import ProxiedRegion
|
|
from hippolyzer.lib.proxy.sessions import Session
|
|
|
|
|
|
PRIM_SCALE = 0.2
|
|
|
|
|
|
class PixelArtistAddon(BaseAddon):
|
|
@handle_command()
|
|
async def import_pixel_art(self, session: Session, region: ProxiedRegion):
|
|
"""
|
|
Import a small image (like a nintendo sprite) and create it out of cube prims
|
|
"""
|
|
filename = await AddonManager.UI.open_file(
|
|
"Open an image",
|
|
filter_str="Images (*.png *.jpg *.jpeg *.bmp)",
|
|
)
|
|
if not filename:
|
|
return
|
|
img = QImage()
|
|
with open(filename, "rb") as f:
|
|
img.loadFromData(f.read(), format=None)
|
|
img = img.convertToFormat(QImage.Format_RGBA8888)
|
|
height = img.height()
|
|
width = img.width()
|
|
pixels: List[Optional[bytes]] = []
|
|
needed_prims = 0
|
|
for y in range(height):
|
|
for x in range(width):
|
|
color: int = img.pixel(x, y)
|
|
# This will be ARGB, SL wants RGBA
|
|
alpha = (color & 0xFF000000) >> 24
|
|
color = color & 0x00FFFFFF
|
|
if alpha > 20:
|
|
# Repack RGBA to the bytes format we use for colors
|
|
pixels.append(struct.pack("!I", (color << 8) | alpha))
|
|
needed_prims += 1
|
|
else:
|
|
# Pretty transparent, skip it
|
|
pixels.append(None)
|
|
|
|
if not await AddonManager.UI.confirm("Confirm prim use", f"This will take {needed_prims} prims"):
|
|
return
|
|
|
|
agent_obj = region.objects.lookup_fullid(session.agent_id)
|
|
agent_pos = agent_obj.RegionPosition
|
|
|
|
created_prims = []
|
|
# Watch for any newly created prims, this is basically what the viewer does to find
|
|
# prims that it just created with the build tool.
|
|
with session.objects.events.subscribe_async(
|
|
(ObjectUpdateType.OBJECT_UPDATE,),
|
|
predicate=lambda e: e.object.UpdateFlags & JUST_CREATED_FLAGS and "LocalID" in e.updated
|
|
) as get_events:
|
|
# Create a pool of prims to use for building the pixel art
|
|
for _ in range(needed_prims):
|
|
# TODO: We don't track the land group or user's active group, so
|
|
# "anyone can build" must be on for rezzing to work.
|
|
group_id = UUID()
|
|
region.circuit.send(Message(
|
|
'ObjectAdd',
|
|
Block('AgentData', AgentID=session.agent_id, SessionID=session.id, GroupID=group_id),
|
|
Block(
|
|
'ObjectData',
|
|
PCode=PCode.PRIMITIVE,
|
|
Material=MCode.WOOD,
|
|
AddFlags=ObjectUpdateFlags.CREATE_SELECTED,
|
|
PathCurve=16,
|
|
ProfileCurve=1,
|
|
PathScaleX=100,
|
|
PathScaleY=100,
|
|
BypassRaycast=1,
|
|
RayStart=agent_obj.RegionPosition + Vector3(0, 0, 2),
|
|
RayEnd=agent_obj.RegionPosition + Vector3(0, 0, 2),
|
|
RayTargetID=UUID(),
|
|
RayEndIsIntersection=0,
|
|
Scale=Vector3(PRIM_SCALE, PRIM_SCALE, PRIM_SCALE),
|
|
Rotation=Quaternion(0.0, 0.0, 0.0, 1.0),
|
|
fill_missing=True,
|
|
),
|
|
))
|
|
# Don't spam a ton of creates at once
|
|
await asyncio.sleep(0.02)
|
|
|
|
# Read any creation events that queued up while we were creating the objects
|
|
# So we can figure out the newly-created objects' IDs
|
|
for _ in range(needed_prims):
|
|
evt: ObjectEvent = await asyncio.wait_for(get_events(), 1.0)
|
|
created_prims.append(evt.object)
|
|
|
|
# Drawing origin starts at the top left, should be positioned just above the
|
|
# avatar on Z and centered on Y.
|
|
top_left = Vector3(0, (width * PRIM_SCALE) * -0.5, (height * PRIM_SCALE) + 2.0) + agent_pos
|
|
positioning_blocks = []
|
|
prim_idx = 0
|
|
for i, pixel_color in enumerate(pixels):
|
|
# Transparent, skip
|
|
if pixel_color is None:
|
|
continue
|
|
x = i % width
|
|
y = i // width
|
|
obj = created_prims[prim_idx]
|
|
# Set a blank texture on all faces
|
|
te = TextureEntryCollection()
|
|
te.Textures[None] = UUID('5748decc-f629-461c-9a36-a35a221fe21f')
|
|
# Set the prim color to the color from the pixel
|
|
te.Color[None] = pixel_color
|
|
# Set the prim texture and color
|
|
region.circuit.send(Message(
|
|
'ObjectImage',
|
|
Block('AgentData', AgentID=session.agent_id, SessionID=session.id),
|
|
Block('ObjectData', ObjectLocalID=obj.LocalID, MediaURL=b'', TextureEntry_=te),
|
|
direction=Direction.OUT,
|
|
))
|
|
# Save the repositioning data for later since it uses a different message,
|
|
# but it can be set in batches.
|
|
positioning_blocks.append(Block(
|
|
'ObjectData',
|
|
ObjectLocalID=obj.LocalID,
|
|
Type=MultipleObjectUpdateFlags.POSITION,
|
|
Data_={'POSITION': top_left + Vector3(0, x * PRIM_SCALE, y * -PRIM_SCALE)},
|
|
))
|
|
await asyncio.sleep(0.01)
|
|
# We actually used a prim for this, so increment the index
|
|
prim_idx += 1
|
|
|
|
# Move the "pixels" to their correct position in chunks
|
|
for chunk in to_chunks(positioning_blocks, 25):
|
|
region.circuit.send(Message(
|
|
'MultipleObjectUpdate',
|
|
Block('AgentData', AgentID=session.agent_id, SessionID=session.id),
|
|
*chunk,
|
|
direction=Direction.OUT,
|
|
))
|
|
await asyncio.sleep(0.01)
|
|
|
|
|
|
addons = [PixelArtistAddon()]
|