diff --git a/packages/frontend/assets/room/objects/tabletop-glass-picture-frame/tabletop-glass-picture-frame.blend b/packages/frontend/assets/room/objects/tabletop-glass-picture-frame/tabletop-glass-picture-frame.blend new file mode 100644 index 0000000000..38a05218b6 Binary files /dev/null and b/packages/frontend/assets/room/objects/tabletop-glass-picture-frame/tabletop-glass-picture-frame.blend differ diff --git a/packages/frontend/assets/room/objects/tabletop-glass-picture-frame/tabletop-glass-picture-frame.glb b/packages/frontend/assets/room/objects/tabletop-glass-picture-frame/tabletop-glass-picture-frame.glb new file mode 100644 index 0000000000..59d6c2021c Binary files /dev/null and b/packages/frontend/assets/room/objects/tabletop-glass-picture-frame/tabletop-glass-picture-frame.glb differ diff --git a/packages/frontend/src/utility/room/object-defs.ts b/packages/frontend/src/utility/room/object-defs.ts index 81a7aba9df..b054928069 100644 --- a/packages/frontend/src/utility/room/object-defs.ts +++ b/packages/frontend/src/utility/room/object-defs.ts @@ -69,6 +69,7 @@ import { steelRack } from './objects/steelRack.js'; import { tabletopCalendar } from './objects/tabletopCalendar.js'; import { tabletopDigitalClock } from './objects/tabletopDigitalClock.js'; import { tabletopFlag } from './objects/tabletopFlag.js'; +import { tabletopGlassPictureFrame } from './objects/tabletopGlassPictureFrame.js'; import { tabletopPictureFrame } from './objects/tabletopPictureFrame.js'; import { tapestry } from './objects/tapestry.js'; import { tetrapod } from './objects/tetrapod.js'; @@ -147,6 +148,7 @@ export const OBJECT_DEFS = [ tabletopCalendar, tabletopDigitalClock, tabletopFlag, + tabletopGlassPictureFrame, tabletopPictureFrame, tapestry, tetrapod, diff --git a/packages/frontend/src/utility/room/objects/tabletopGlassPictureFrame.ts b/packages/frontend/src/utility/room/objects/tabletopGlassPictureFrame.ts new file mode 100644 index 0000000000..bff0ad2bb0 --- /dev/null +++ b/packages/frontend/src/utility/room/objects/tabletopGlassPictureFrame.ts @@ -0,0 +1,137 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as BABYLON from '@babylonjs/core'; +import { defineObject } from '../engine.js'; +import { createPlaneUvMapper, getPlaneUvIndexes } from '../utility.js'; + +const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => { + return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin); +}; + +export const tabletopGlassPictureFrame = defineObject({ + id: 'tabletopGlassPictureFrame', + name: 'tabletopGlassPictureFrame', + options: { + schema: { + width: { + type: 'range', + label: 'Width', + min: 0, + max: 1, + step: 0.01, + }, + height: { + type: 'range', + label: 'Height', + min: 0, + max: 1, + step: 0.01, + }, + customPicture: { + type: 'image', + label: 'Custom picture', + }, + fit: { + type: 'enum', + label: 'Custom picture fit', + enum: ['cover', 'contain', 'stretch'], + }, + }, + default: { + width: 0.15, + height: 0.15, + customPicture: null, + fit: 'cover', + }, + }, + placement: 'top', + createInstance: async ({ scene, options, model }) => { + const pictureMesh = model.findMesh('__X_PICTURE__'); + const frameMesh = model.findMesh('__X_FRAME__'); + const pinMeshes = model.findMeshes('__X_PIN__'); + const pictureMaterial = model.findMaterial('__X_PICTURE__'); + + const updateUv = createPlaneUvMapper(pictureMesh); + + const applyFit = () => { + const tex = pictureMaterial.albedoTexture; + if (tex == null) return; + + const srcWidth = tex.getSize().width; + const srcHeight = tex.getSize().height; + const srcAspect = srcWidth / srcHeight; + const targetWidth = remap(options.width, 0, 1, 2, 100); // 最小値(値を0にした場合)でのサイズは2cmで、最大値(値を1にした場合)でのサイズは100cmなので。比率の計算だから単位はなんでもいいけど、とにかく0が0にならない点を考慮させる必要がある + const targetHeight = remap(options.height, 0, 1, 2, 100); // 最小値(値を0にした場合)でのサイズは2cmで、最大値(値を1にした場合)でのサイズは100cmなので。比率の計算だから単位はなんでもいいけど、とにかく0が0にならない点を考慮させる必要がある + const targetAspect = targetWidth / targetHeight; + + updateUv(srcAspect, targetAspect, options.fit); + + model.updated(); + }; + + applyFit(); + + const applySize = () => { + pictureMesh.morphTargetManager!.getTargetByName('Width')!.influence = options.width; + pictureMesh.morphTargetManager!.getTargetByName('Height')!.influence = options.height; + frameMesh.morphTargetManager!.getTargetByName('Width')!.influence = options.width; + frameMesh.morphTargetManager!.getTargetByName('Height')!.influence = options.height; + for (const pinMesh of pinMeshes) { + pinMesh.morphTargetManager!.getTargetByName('Width')!.influence = options.width; + pinMesh.morphTargetManager!.getTargetByName('Height')!.influence = options.height; + } + model.updated(); + + applyFit(); + }; + + applySize(); + + const applyCustomPicture = () => new Promise((resolve) => { + if (options.customPicture != null) { + const tex = new BABYLON.Texture(options.customPicture, scene, false, false); + tex.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODE; + tex.wrapV = BABYLON.Texture.MIRROR_ADDRESSMODE; + + pictureMaterial.unfreeze(); + pictureMaterial.albedoColor = new BABYLON.Color3(1, 1, 1); + pictureMaterial.albedoTexture = tex; + + tex.onLoadObservable.addOnce(() => { + applyFit(); + resolve(); + }); + } else { + pictureMaterial.albedoColor = new BABYLON.Color3(0.5, 0.5, 0.5); + pictureMaterial.albedoTexture = null; + resolve(); + } + }); + + await applyCustomPicture(); + + return { + onInited: () => { + + }, + onOptionsUpdated: ([k, v]) => { + switch (k) { + case 'width': + case 'height': + applySize(); + break; + case 'customPicture': + applyCustomPicture(); + break; + case 'fit': + applyFit(); + break; + } + }, + interactions: {}, + }; + }, +});