1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-07-26 05:35:22 +02:00

attachments

This commit is contained in:
syuilo
2026-05-14 12:43:30 +09:00
parent 410cc8ac50
commit ef8f593de8
27 changed files with 199 additions and 66 deletions

View File

@@ -29,6 +29,7 @@ import * as Misskey from 'misskey-js';
import MkDrive from '@/components/MkDrive.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
import { deepClone } from '@/utility/clone.js';
withDefaults(defineProps<{
initialFolder?: Misskey.entities.DriveFolder['id'] | null;
@@ -46,7 +47,7 @@ const dialog = useTemplateRef('dialog');
const selected = ref<Misskey.entities.DriveFile[]>([]);
function ok() {
emit('done', selected.value);
emit('done', deepClone(selected.value)); // selected.valueをそのまま渡すと純粋なオブジェクトではなくProxyが渡されてしまい、シチュエーションによってはそれによって不具合の原因になる(workerにpostMessageで渡せないなど)
dialog.value?.close();
}

View File

@@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:leaveToClass="prefer.s.animation ? $style.transition_options_leaveTo : ''"
>
<div v-if="selectedObjectDef != null && selectedInstanceId != null && showObjectOptions" :class="$style.customize" class="_panel _shadow">
<XObjectCustomizeForm :schema="selectedObjectDef.options.schema" :options="selectedObjectOptionsState" @update="(k, v) => updateObjectOption(k, v)"></XObjectCustomizeForm>
<XObjectCustomizeForm :addFileAttachment="addFileAttachment" :schema="selectedObjectDef.options.schema" :options="selectedObjectOptionsState" @update="(k, v) => updateObjectOption(k, v)"></XObjectCustomizeForm>
</div>
</Transition>
</div>
@@ -66,10 +66,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts">
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive, nextTick, shallowRef, computed, triggerRef, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import XObjectCustomizeForm from './room.object-customize-form.vue';
import XItem from './room.add-object-dialog.item.vue';
import type { RoomObjectInstance, RoomStateObject } from '@/world/room/object.js';
import type { RawOptions, RoomObjectInstance, RoomStateObject } from '@/world/room/object.js';
import type { PreviewEngineControllerOptions } from '@/world/room/previewEngineController.js';
import type { RoomAttachments } from '@/world/room/utility.js';
import { i18n } from '@/i18n.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import * as os from '@/os.js';
@@ -93,6 +95,7 @@ const emit = defineEmits<{
(ev: 'ok', ctx: {
id: string;
options: any;
attachments: RoomAttachments;
}): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
@@ -103,11 +106,19 @@ const canvas = useTemplateRef('canvas');
const selectedId = ref<string | null>(null);
const showPreview = ref(false);
const selectedInstanceId = ref<string | null>(null);
const selectedObjectOptionsState = ref<RoomStateObject | null>(null);
const selectedObjectOptionsState = ref<RawOptions | null>(null);
const selectedObjectDef = computed(() => OBJECT_DEFS.find(def => def.id === selectedId.value) ?? null);
const showObjectOptions = ref(false);
const searchKeyword = ref('');
const attachments = {
files: [],
} as RoomAttachments;
function addFileAttachment(file: Misskey.entities.DriveFile) {
attachments.files.push(file);
}
const previewEngineControllerOptions = computed<PreviewEngineControllerOptions>(() => ({
graphicsQuality: props.graphicsQuality,
fps: null,
@@ -179,7 +190,7 @@ watch(selectedId, (newId) => {
});
function updateObjectOption(k: string, v: any) {
controller.updateObjectOption(k, v);
controller.updateObjectOption(k, v, attachments);
selectedObjectOptionsState.value![k] = v;
}
@@ -195,6 +206,7 @@ function ok() {
emit('ok', {
id: selectedId.value,
options: deepClone(selectedObjectOptionsState.value),
attachments: deepClone(attachments),
});
dialog.value?.close();

View File

@@ -25,7 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkRange :continuousUpdate="true" :min="s.min" :max="s.max" :step="s.step" :modelValue="options[k]" @update:modelValue="v => emit('update', k, v)"></MkRange>
</div>
<div v-else-if="s.type === 'image'">
<MkInput type="text" :modelValue="options[k]" @update:modelValue="v => emit('update', k, v)"></MkInput>
<MkButton primary inline @click="changeImage(k)">Change</MkButton>
<MkButton v-if="options[k] != null" danger inline iconOnly @click="clearImage(k)"><i class="ti ti-x"></i></MkButton>
</div>
<div v-else-if="s.type === 'seed'">
<MkRange :continuousUpdate="true" :min="0" :max="1000" :step="1" :modelValue="options[k]" @update:modelValue="v => emit('update', k, v)"></MkRange>
@@ -37,6 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import type { ObjectDef } from '@/world/room/object.js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
@@ -46,15 +48,44 @@ import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.vue';
import { getHex, getRgb } from '@/world/utility.js';
import { chooseDriveFile } from '@/utility/drive.js';
const props = defineProps<{
schema: ObjectDef['options']['schema'];
options: any;
addFileAttachment: ((file: Misskey.entities.DriveFile) => void);
}>();
const emit = defineEmits<{
(ev: 'update', k: string, v: any): void;
}>();
async function changeImage(k: string) {
chooseDriveFile({ multiple: false }).then((fileResponse) => {
if (fileResponse.length === 0) return;
const file = fileResponse[0];
if (!file.type.startsWith('image/')) {
os.alert({
type: 'error',
text: 'The selected file is not an image.',
});
return;
}
if (file.size > 1024 * 1024 * 5) {
os.alert({
type: 'error',
text: 'The file size exceeds the limit of 5MB.',
});
return;
}
props.addFileAttachment(file);
emit('update', k, file.id);
});
}
function clearImage(k: string) {
emit('update', k, null);
}
</script>
<style lang="scss" module>

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root">
<div :class="[$style.screen, { [$style.zen]: false }]">
<canvas ref="canvas" :class="$style.canvas" tabindex="-1" :style="{ visibility: controller.isReady.value ? 'visible' : 'hidden' }"></canvas>
<canvas ref="canvas" :class="$style.canvas" tabindex="-1"></canvas>
<Transition
:enterActiveClass="$style.transition_fade_enterActive"
@@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="controller.isReady.value && controller.isEditMode.value && controller.selected.value != null && selectedObjectDef != null && !controller.grabbing.value" :key="controller.selected.value.objectId" class="_panel" :class="$style.overlayObjectInfoPanel">
{{ selectedObjectDef.name }}
<XObjectCustomizeForm :schema="selectedObjectDef.options.schema" :options="controller.selected.value.objectState.options" @update="(k, v) => controller.updateObjectOption(controller.selected.value.objectId, k, v)"></XObjectCustomizeForm>
<XObjectCustomizeForm :addFileAttachment="addFileAttachment" :schema="selectedObjectDef.options.schema" :options="controller.selected.value.objectState.options" @update="(k, v) => controller.updateObjectOption(controller.selected.value.objectId, k, v, attachments)"></XObjectCustomizeForm>
</div>
<div v-if="isRoomSettingsOpen && controller.isEditMode.value" class="_panel" :class="$style.overlayObjectInfoPanel">
@@ -88,9 +88,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, defineAsyncComponent, markRaw, nextTick, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue';
import * as BABYLON from '@babylonjs/core';
import * as Misskey from 'misskey-js';
import XObjectCustomizeForm from './room.object-customize-form.vue';
import XEnvOptions from './room.env-options.vue';
import type { RoomControllerOptions } from '@/world/room/controller.js';
import type { RoomAttachments } from '@/world/room/utility.js';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { ensureSignin } from '@/i';
@@ -258,6 +260,14 @@ console.log('installedObjects:', data.installedObjects.length);
let latestData = deepClone(data);
const attachments = {
files: [],
} as RoomAttachments;
function addFileAttachment(file: Misskey.entities.DriveFile) {
attachments.files.push(file);
}
const roomControllerOptions = computed<RoomControllerOptions>(() => ({
graphicsQuality: graphicsQuality.value,
fps: fps.value,
@@ -282,7 +292,7 @@ onMounted(async () => {
}
try {
await controller.init(canvas.value!);
await controller.init(canvas.value!, attachments);
} catch (err) {
console.error(err);
os.alert({
@@ -457,7 +467,8 @@ async function addObject(ev: PointerEvent) {
graphicsQuality: graphicsQuality.value,
}, {
ok: async (res) => {
controller.addObject(res.id, res.options);
attachments.files.push(...res.attachments.files); // TODO: mergeAttachmentsみたいな関数を実装して使う
controller.addObject(res.id, res.options, attachments);
canvas.value!.focus();
},
closed: () => {

View File

@@ -9,8 +9,10 @@ import { EngineControllerBase } from '../engineControllerBase.js';
import type { ShallowRef } from 'vue';
import type { RoomState } from './engine.js';
import type { RoomStateObject } from './object.js';
import type { RoomAttachments } from './utility.js';
import * as sound from '@/utility/sound.js';
import { deepEqual } from '@/utility/deep-equal.js';
import { deepClone } from '@/utility/clone.js';
export type RoomControllerOptions = {
workerMode?: boolean;
@@ -39,18 +41,18 @@ export class RoomController extends EngineControllerBase {
this.roomState = shallowRef(roomState);
}
public async init(canvas: HTMLCanvasElement) {
public async init(canvas: HTMLCanvasElement, attachments: RoomAttachments) {
const { engineEvents } = await this._init_(canvas, {
createWorker: (offscreen) => new Promise((resolve) => {
import('./worker?worker').then(({ default: RoomWorker }) => {
const worker = new RoomWorker();
worker.postMessage({ type: 'init', canvas: offscreen, options: this.options, roomState: this.roomState.value }, [offscreen]);
worker.postMessage({ type: 'init', canvas: offscreen, options: this.options, roomState: this.roomState.value, roomAttachments: attachments }, [offscreen]);
resolve(worker);
});
}),
createEngine: (babylonEngine) => new Promise((resolve) => {
import('./engine.js').then(({ RoomEngine }) => {
resolve(new RoomEngine(this.roomState.value, {
resolve(new RoomEngine(this.roomState.value, attachments, {
engine: babylonEngine,
...this.options,
}));
@@ -138,8 +140,8 @@ export class RoomController extends EngineControllerBase {
this.set('gridSnapping', gridSnapping);
}
public updateObjectOption(objectId: string, key: string, value: any) {
this.call('updateObjectOption', [objectId, key, value]);
public updateObjectOption(objectId: string, key: string, value: any, attachments?: RoomAttachments) {
this.call('updateObjectOption', deepClone([objectId, key, value, attachments])); // 場合によってはvueによって(postMessage不能な)proxy化された値が来ることも考えられるのでdeepClone
}
public changeEnvType(type: RoomState['env']['type']) {
@@ -162,8 +164,8 @@ export class RoomController extends EngineControllerBase {
this.call('removeSelectedObject');
}
public addObject(type: string, options: any) {
this.call('addObject', [type, options]);
public addObject(type: string, options: any, attachments?: RoomAttachments) {
this.call('addObject', deepClone([type, options, attachments])); // 場合によってはvueによって(postMessage不能な)proxy化された値が来ることも考えられるのでdeepClone
}
public endGrabbing() {

View File

@@ -17,9 +17,11 @@ import { TIME_MAP, scaleMorph, camelToKebab, cm, WORLD_SCALE, getMeshesBoundingB
import { getObjectDef } from './object-defs.js';
import { findMaterial, GRAPHICS_QUALITY, ModelManager, SYSTEM_HEYA_MESH_NAMES, SYSTEM_MESH_NAMES } from './utility.js';
import { JapaneseEnvManager, MuseumEnvManager, SimpleEnvManager } from './env.js';
import { convertRawOptions } from './object.js';
import type { RoomAttachments } from './utility.js';
import type { ConvertedOptions, ObjectDef, RoomObjectInstance, RoomStateObject } from './object.js';
import type { GridMaterial } from '@babylonjs/materials';
import type { EnvManager, JapaneseEnvOptions, SimpleEnvOptions } from './env.js';
import type { ObjectDef, RoomObjectInstance, RoomStateObject } from './object.js';
import { genId } from '@/utility/id.js';
import { deepClone } from '@/utility/clone.js';
@@ -36,7 +38,7 @@ export type RoomState = {
options: JapaneseEnvOptions;
};
roomLightColor: [number, number, number];
installedObjects: RoomStateObject<any>[];
installedObjects: RoomStateObject[];
worldScale: number;
};
@@ -97,7 +99,7 @@ export type RoomEngineEvents = {
'changeSelectedState': (ctx: {
selected: {
objectId: string;
objectState: RoomStateObject<any>;
objectState: RoomStateObject;
} | null;
}) => void;
'changeGrabbingState': (ctx: { grabbing: { forInstall: boolean } | null }) => void;
@@ -123,6 +125,7 @@ export class RoomEngine extends EventEmitter {
private fixedCamera: BABYLON.FreeCamera;
public objectEntities: Map<string, {
rootMesh: BABYLON.Mesh;
convertedOptions: ConvertedOptions;
instance: RoomObjectInstance;
model: ModelManager;
}> = new Map();
@@ -156,7 +159,7 @@ export class RoomEngine extends EventEmitter {
private _selected: {
objectId: string;
objectEntity: RoomEngine['objectEntities'] extends Map<string, infer V> ? V : never;
objectState: RoomStateObject<any>;
objectState: RoomStateObject;
objectDef: ObjectDef;
} | null = null;
get selected() {
@@ -169,6 +172,7 @@ export class RoomEngine extends EventEmitter {
private time: 0 | 1 | 2 = 0; // 0: 昼, 1: 夕, 2: 夜
public roomState: RoomState;
public roomAttachments: RoomAttachments;
private _gridSnapping = { enabled: true, scale: cm(4) };
get gridSnapping() {
@@ -213,7 +217,7 @@ export class RoomEngine extends EventEmitter {
'pointer': (event: { x: number; y: number; }) => void;
}> = new EventEmitter();
constructor(roomState: RoomState, options: {
constructor(roomState: RoomState, roomAttachments: RoomAttachments, options: {
engine: BABYLON.WebGPUEngine;
graphicsQuality: number;
fps: number | null;
@@ -229,6 +233,7 @@ export class RoomEngine extends EventEmitter {
options: { ...getObjectDef(o.type).options.default, ...o.options },
})),
};
this.roomAttachments = roomAttachments;
this.graphicsQuality = options.graphicsQuality;
this.fps = options.fps;
this.useGlow = this.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM;
@@ -907,6 +912,8 @@ export class RoomEngine extends EventEmitter {
*/
});
const convertedOptions = convertRawOptions(def.options.schema, args.options, this.roomAttachments);
const objectInstance = await def.createInstance({
scene: this.scene,
sr: {
@@ -923,7 +930,7 @@ export class RoomEngine extends EventEmitter {
},
lc: this.lightContainer,
root,
options: args.options,
options: convertedOptions,
model,
id: args.id,
timer: this.timer, // TODO: 家具が撤去された後も動作し続けるのをどうにかする
@@ -960,7 +967,7 @@ export class RoomEngine extends EventEmitter {
enableObjectCollision(root.getChildMeshes());
}
this.objectEntities.set(args.id, { instance: objectInstance, rootMesh: root, model });
this.objectEntities.set(args.id, { convertedOptions, instance: objectInstance, rootMesh: root, model });
return { root, objectInstance };
}
@@ -1441,9 +1448,14 @@ export class RoomEngine extends EventEmitter {
return root;
}
public async addObject(type: string, _options?: any) {
public async addObject(type: string, _options?: any, attachments?: RoomAttachments) {
if (!this.isEditMode) return;
if (this.grabbingCtx != null) return;
if (attachments != null) {
this.roomAttachments = attachments;
}
this.selectObject(null);
const dir = this.camera.getDirection(BABYLON.Axis.Z).scale(this.scene.useRightHandedSystem ? -1 : 1);
@@ -1683,18 +1695,27 @@ export class RoomEngine extends EventEmitter {
this.grabbingCtx.rotation += delta;
}
public updateObjectOption(objectId: string, key: string, value: any) {
const options = this.roomState.installedObjects.find(o => o.id === objectId)?.options;
if (options == null) return;
options[key] = value;
public updateObjectOption(objectId: string, key: string, value: any, attachments?: RoomAttachments) {
if (attachments != null) {
this.roomAttachments = attachments;
}
const o = this.roomState.installedObjects.find(o => o.id === objectId);
if (o == null) return;
const def = getObjectDef(o.type);
o.options[key] = value;
this.ev('changeRoomState', { roomState: this.roomState });
const entity = this.objectEntities.get(objectId);
if (entity == null) return;
const converted = convertRawOptions(def.options.schema, o.options, this.roomAttachments);
entity.convertedOptions[key] = converted[key];
this.sr.disableSnapshotRendering();
entity.instance.onOptionsUpdated?.([key, value]);
entity.instance.onOptionsUpdated?.([key, converted[key]]);
this.sr.enableSnapshotRendering();
}

View File

@@ -4,17 +4,16 @@
*/
import * as BABYLON from '@babylonjs/core';
import type { RoomEngine } from './engine.js';
import type { ModelManager } from './utility.js';
import type { ModelManager, RoomAttachments } from './utility.js';
import type { Timer } from '../utility.js';
// babylonのドメイン知識は持たない
export type RoomStateObject<Options = any> = {
export type RoomStateObject = {
id: string;
type: string;
position: [number, number, number];
rotation: [number, number, number];
options: Options;
options: RawOptions;
/**
* 別のオブジェクトのID
@@ -83,7 +82,15 @@ type SeedOptionSchema = {
type OptionsSchema = Record<string, NumberOptionSchema | BooleanOptionSchema | StringOptionSchema | ColorOptionSchema | EnumOptionSchema | RangeOptionSchema | ImageOptionSchema | SeedOptionSchema>;
type GetOptionsSchemaValues<T extends OptionsSchema> = {
export type RawOptions = Record<string, unknown> & {
readonly __brand: unique symbol;
};
export type ConvertedOptions = Record<string, unknown> & {
readonly __brand: unique symbol;
};
type GetRawOptionsSchemaValues<T extends OptionsSchema> = {
[K in keyof T]:
T[K] extends NumberOptionSchema ? number :
T[K] extends BooleanOptionSchema ? boolean :
@@ -95,6 +102,18 @@ type GetOptionsSchemaValues<T extends OptionsSchema> = {
T[K] extends SeedOptionSchema ? number :
never;
};
type GetConvertedOptionsSchemaValues<T extends OptionsSchema> = {
[K in keyof T]:
T[K] extends NumberOptionSchema ? number :
T[K] extends BooleanOptionSchema ? boolean :
T[K] extends StringOptionSchema ? string :
T[K] extends ColorOptionSchema ? [number, number, number] :
T[K] extends EnumOptionSchema ? T[K]['enum'][number] :
T[K] extends RangeOptionSchema ? number :
T[K] extends ImageOptionSchema ? { url: string; } | null :
T[K] extends SeedOptionSchema ? number :
never;
};
export type ObjectDef<OpSc extends OptionsSchema = OptionsSchema> = {
id: string;
@@ -102,7 +121,7 @@ export type ObjectDef<OpSc extends OptionsSchema = OptionsSchema> = {
path?: string;
options: {
schema: OpSc;
default: GetOptionsSchemaValues<OpSc>;
default: GetRawOptionsSchemaValues<OpSc>;
};
placement: 'top' | 'side' | 'bottom' | 'wall' | 'ceiling' | 'floor';
hasCollisions?: boolean;
@@ -122,13 +141,13 @@ export type ObjectDef<OpSc extends OptionsSchema = OptionsSchema> = {
};
lc: BABYLON.ClusteredLightContainer | null;
root: BABYLON.Mesh;
options: Readonly<GetOptionsSchemaValues<OpSc>>;
options: Readonly<GetConvertedOptionsSchemaValues<OpSc>>;
model: ModelManager;
id: string;
timer: Timer;
graphicsQuality: number;
stickyMarkerMeshUpdated?: (mesh: BABYLON.Mesh) => void;
}) => RoomObjectInstance<GetOptionsSchemaValues<OpSc>> | Promise<RoomObjectInstance<GetOptionsSchemaValues<OpSc>>>; // TODO: createInstanceをasyncにするのではなく、別にreadyみたいなものを返させる
}) => RoomObjectInstance<GetConvertedOptionsSchemaValues<OpSc>> | Promise<RoomObjectInstance<GetConvertedOptionsSchemaValues<OpSc>>>; // TODO: createInstanceをasyncにするのではなく、別にreadyみたいなものを返させる
};
export function defineObject<const OpSc extends OptionsSchema>(def: ObjectDef<OpSc>): ObjectDef<OpSc> {
@@ -142,3 +161,26 @@ export function defineObjectClass<const OpSc extends OptionsSchema>(baseDef: Par
extend: (childDef) => ({ ...baseDef, ...childDef }) as ObjectDef<OpSc>,
};
}
export function convertRawOptions<OpSc extends OptionsSchema>(schema: OpSc, raw: RawOptions, attachments: RoomAttachments): GetConvertedOptionsSchemaValues<OpSc> {
const converted = {} as GetConvertedOptionsSchemaValues<OpSc>;
for (const record of Object.entries(schema)) {
const k = record[0];
const v = raw[k];
if (record[1].type === 'image' && v != null) {
if (v === '') {
converted[k] = null;
continue;
}
const file = attachments.files.find(f => f.id === v);
if (file != null) {
converted[k] = { url: file.url };
} else {
converted[k] = null;
}
} else {
converted[k] = v;
}
}
return converted;
}

View File

@@ -97,7 +97,7 @@ export const allInOnePc = defineObject({
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null) {
screenMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
screenMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
screenMaterial.emissiveTexture = tex;
applyFit();

View File

@@ -90,7 +90,7 @@ export const clippedPicture = defineObject({
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null) {
pictureMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
pictureMaterial.albedoColor = new BABYLON.Color3(1, 1, 1);
pictureMaterial.albedoTexture = tex;
applyFit();

View File

@@ -68,9 +68,9 @@ export const djPlayer = defineObject({
applyScreenBrightness();
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null && options.customPicture !== '') {
if (options.customPicture != null) {
screenMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
screenMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
screenMaterial.emissiveTexture = tex;
applyFit();

View File

@@ -79,7 +79,7 @@ export const handheldGameConsole = defineObject({
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null) {
screenMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
screenMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
screenMaterial.emissiveTexture = tex;
applyFit();

View File

@@ -107,7 +107,7 @@ export const laptopPc = defineObject({
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null) {
screenMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
screenMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
screenMaterial.emissiveTexture = tex;
applyFit();

View File

@@ -53,7 +53,7 @@ export const largeMousepad = defineObject({
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null) {
padMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
padMaterial.albedoColor = new BABYLON.Color3(1, 1, 1);
padMaterial.albedoTexture = tex;
applyFit();

View File

@@ -92,7 +92,7 @@ export const monitor = defineObject({
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null) {
screenMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
screenMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
screenMaterial.emissiveTexture = tex;
applyFit();

View File

@@ -175,7 +175,7 @@ export const pictureFrame = defineObject({
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null) {
pictureMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
pictureMaterial.albedoColor = new BABYLON.Color3(1, 1, 1);
pictureMaterial.albedoTexture = tex;
applyFit();

View File

@@ -95,7 +95,7 @@ export const poster = defineObject({
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null) {
pictureMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
pictureMaterial.albedoColor = new BABYLON.Color3(1, 1, 1);
pictureMaterial.albedoTexture = tex;
applyFit();

View File

@@ -54,7 +54,7 @@ export const tabletopFlag = defineObject({
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null) {
flagMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
flagMaterial.albedoColor = new BABYLON.Color3(1, 1, 1);
flagMaterial.albedoTexture = tex;
applyFit();

View File

@@ -95,7 +95,7 @@ export const tabletopGlassPictureFrame = defineObject({
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null) {
pictureMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
pictureMaterial.albedoColor = new BABYLON.Color3(1, 1, 1);
pictureMaterial.albedoTexture = tex;
applyFit();

View File

@@ -74,9 +74,9 @@ export const tabletopLcdButtonsController = defineObject({
applyFit();
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null && options.customPicture !== '') {
if (options.customPicture != null) {
screenMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
screenMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
screenMaterial.emissiveTexture = tex;
applyFit();

View File

@@ -168,7 +168,7 @@ export const tabletopPictureFrame = defineObject({
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null) {
pictureMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
pictureMaterial.albedoColor = new BABYLON.Color3(1, 1, 1);
pictureMaterial.albedoTexture = tex;
applyFit();

View File

@@ -99,7 +99,7 @@ export const tapestry = defineObject({
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null) {
pictureMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
pictureMaterial.albedoColor = new BABYLON.Color3(1, 1, 1);
pictureMaterial.albedoTexture = tex;
applyFit();

View File

@@ -85,7 +85,7 @@ export const wallCanvas = defineObject({
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null) {
canvasMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
canvasMaterial.albedoColor = new BABYLON.Color3(1, 1, 1);
canvasMaterial.albedoTexture = tex;
applyFit();

View File

@@ -95,7 +95,7 @@ export const wallGlassPictureFrame = defineObject({
const applyCustomPicture = () => new Promise<void>((resolve) => {
if (options.customPicture != null) {
pictureMaterial.unfreeze();
const tex = new BABYLON.Texture(options.customPicture, scene, false, false, undefined, () => {
const tex = new BABYLON.Texture(options.customPicture.url, scene, false, false, undefined, () => {
pictureMaterial.albedoColor = new BABYLON.Color3(1, 1, 1);
pictureMaterial.albedoTexture = tex;
applyFit();

View File

@@ -10,7 +10,9 @@ import EventEmitter from 'eventemitter3';
import { camelToKebab, WORLD_SCALE, cm, getMeshesBoundingBox, Timer, sleep, ArcRotateCameraManualInput } from '../utility.js';
import { getObjectDef } from './object-defs.js';
import { SYSTEM_MESH_NAMES, ModelManager, GRAPHICS_QUALITY } from './utility.js';
import type { RoomObjectInstance } from './object.js';
import { convertRawOptions } from './object.js';
import type { ConvertedOptions, RawOptions, RoomObjectInstance } from './object.js';
import type { RoomAttachments } from './utility.js';
import { genId } from '@/utility/id.js';
import { deepClone } from '@/utility/clone.js';
@@ -23,7 +25,8 @@ export class RoomObjectPreviewEngine extends EventEmitter {
private camera: BABYLON.ArcRotateCamera;
private objectMesh: BABYLON.Mesh | null = null;
private objectInstance: RoomObjectInstance | null = null;
private objectOptions: any = null;
private objectOptions: RawOptions | null = null;
private convertedObjectOptions: ConvertedOptions | null = null;
private objectType: string | null = null;
private envMapIndoor: BABYLON.CubeTexture;
private roomLight: BABYLON.SpotLight;
@@ -230,10 +233,10 @@ export class RoomObjectPreviewEngine extends EventEmitter {
this.objectOptions[key] = Math.floor(Math.random() * 1000);
}
}
this.convertedObjectOptions = convertRawOptions(def.options.schema, this.objectOptions, { files: [] });
await this.loadObject_({
type,
options: this.objectOptions,
id,
});
@@ -290,7 +293,6 @@ export class RoomObjectPreviewEngine extends EventEmitter {
private async loadObject_(args: {
type: string;
options: any;
id: string;
}) {
const def = getObjectDef(args.type);
@@ -366,7 +368,7 @@ export class RoomObjectPreviewEngine extends EventEmitter {
fixParticleSystem: (ps) => this.sr.fixParticleSystem(ps),
},
root,
options: args.options,
options: this.convertedObjectOptions,
model,
id: args.id,
timer: this.timerForEachObject,
@@ -380,10 +382,12 @@ export class RoomObjectPreviewEngine extends EventEmitter {
this.objectMesh = root;
}
public updateObjectOption(key: string, value: any) {
public updateObjectOption(key: string, value: any, attachments?: RoomAttachments) {
this.sr.disableSnapshotRendering();
this.objectOptions[key] = value;
this.objectInstance?.onOptionsUpdated?.([key, value]);
const convertedOptions = convertRawOptions(getObjectDef(this.objectType!).options.schema, this.objectOptions, attachments ?? { files: [] });
this.convertedObjectOptions[key] = convertedOptions[key];
this.objectInstance?.onOptionsUpdated?.([key, convertedOptions[key]]);
this.sr.enableSnapshotRendering();
return this.objectOptions;
}
@@ -398,6 +402,7 @@ export class RoomObjectPreviewEngine extends EventEmitter {
this.objectInstance.dispose?.();
this.objectInstance = null;
this.objectOptions = null;
this.convertedObjectOptions = null;
this.objectMesh!.dispose();
this.objectMesh = null;
this.objectType = null;

View File

@@ -4,6 +4,7 @@
*/
import { EngineControllerBase } from '../engineControllerBase.js';
import type { RoomAttachments } from './utility.js';
export type PreviewEngineControllerOptions = {
workerMode?: boolean;
@@ -41,8 +42,8 @@ export class PreviewEngineController extends EngineControllerBase {
});
}
public updateObjectOption(key: string, value: any) {
this.call('updateObjectOption', [key, value]);
public updateObjectOption(key: string, value: any, attachments?: RoomAttachments) {
this.call('updateObjectOption', [key, value, attachments]);
}
public loadObject(type: string) {

View File

@@ -4,6 +4,7 @@
*/
import * as BABYLON from '@babylonjs/core';
import * as Misskey from 'misskey-js';
import { applyMorphTargetsToMesh, cm, getPlaneUvIndexes, Timer } from '../utility.js';
export const GRAPHICS_QUALITY = {
@@ -22,6 +23,10 @@ export function getLightRangeFactorByGraphicsQuality(quality: number) {
}
}
export type RoomAttachments = {
files: Misskey.entities.DriveFile[];
};
export const SYSTEM_MESH_NAMES = ['__TOP__', '__SIDE__', '__BOTTOM__', '__PICK__', '__COLLISION__'];
export const SYSTEM_HEYA_MESH_NAMES = ['__ROOM_WALL__', '__ROOM_SIDE__', '__ROOM_FLOOR__', '__ROOM_CEILING__', '__ROOM_TOP__', '__ROOM_BOTTOM__', '__COLLISION__'];

View File

@@ -6,6 +6,7 @@
import * as BABYLON from '@babylonjs/core';
import { RoomEngine } from './engine.js';
import type { RoomState } from './engine.js';
import type { RoomAttachments } from './utility.js';
let engine: RoomEngine | null = null;
let canvas: OffscreenCanvas | null = null;
@@ -17,6 +18,7 @@ onmessage = async (event) => {
switch (event.data?.type) {
case 'init': {
const roomState = event.data.roomState as RoomState;
const roomAttachments = event.data.roomAttachments as RoomAttachments;
canvas = event.data.canvas as OffscreenCanvas;
const babylonEngine = new BABYLON.WebGPUEngine(canvas, { doNotHandleContextLost: true, powerPreference: 'high-performance', antialias: event.data.options.antialias });
babylonEngine.compatibilityMode = false;
@@ -25,7 +27,7 @@ onmessage = async (event) => {
if (event.data.options.resolution === 2) babylonEngine.setHardwareScalingLevel(0.5);
if (event.data.options.resolution === 0.5) babylonEngine.setHardwareScalingLevel(2);
engine = new RoomEngine(roomState, {
engine = new RoomEngine(roomState, roomAttachments, {
engine: babylonEngine,
...event.data.options,
});