mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-26 05:35:22 +02:00
attachments
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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__'];
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user