1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-20 23:25:28 +02:00
This commit is contained in:
syuilo
2026-02-19 21:38:44 +09:00
parent dadc5295fa
commit 17a3bdb5eb
5 changed files with 160 additions and 35 deletions

View File

@@ -26,14 +26,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="engine != null && engine.isEditMode.value && engine.selected.value != null" class="_panel" :class="$style.overlayObjectInfoPanel"> <div v-if="engine != null && engine.isEditMode.value && engine.selected.value != null" class="_panel" :class="$style.overlayObjectInfoPanel">
{{ engine.selected.value.objectDef.name }} {{ engine.selected.value.objectDef.name }}
</div>
</div>
<div v-if="engine != null" class="_buttons" :class="$style.controls"> <div v-for="[k, s] in Object.entries(engine.selected.value.objectDef.options.schema)" :key="k">
<!--<MkButton v-for="action in actions" :key="action.key" @click="action.fn">{{ action.label }}{{ hotkeyToLabel(action.hotkey) }}</MkButton>--> <div>{{ s.label }}</div>
<MkButton @click="toggleLight">Toggle Light</MkButton> <div v-if="s.type === 'color'">
<MkButton :primary="engine.isEditMode.value" @click="toggleEditMode">Edit mode: {{ engine.isEditMode.value ? 'on' : 'off' }}</MkButton> <MkInput :modelValue="getHex(engine.selected.value.objectState.options[k])" type="color" @update:modelValue="v => { const c = getRgb(v); if (c != null) engine.updateObjectOption(engine.selected.value.objectId, k, c); }"></MkInput>
<MkButton @click="addObject">addObject</MkButton> </div>
</div>
</div>
<div v-if="engine != null" class="_buttons" :class="$style.controls">
<!--<MkButton v-for="action in actions" :key="action.key" @click="action.fn">{{ action.label }}{{ hotkeyToLabel(action.hotkey) }}</MkButton>-->
<MkButton @click="toggleLight">Toggle Light</MkButton>
<MkButton :primary="engine.isEditMode.value" @click="toggleEditMode">Edit mode: {{ engine.isEditMode.value ? 'on' : 'off' }}</MkButton>
<MkButton @click="addObject">addObject</MkButton>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -48,6 +55,7 @@ import { RoomEngine } from '@/utility/room/engine.js';
import { getObjectDef, OBJECT_DEFS } from '@/utility/room/object-defs.js'; import { getObjectDef, OBJECT_DEFS } from '@/utility/room/object-defs.js';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import MkInput from '@/components/MkInput.vue';
const canvas = useTemplateRef('canvas'); const canvas = useTemplateRef('canvas');
@@ -516,6 +524,24 @@ function addObject(ev: PointerEvent) {
})), ev.currentTarget ?? ev.target); })), ev.currentTarget ?? ev.target);
} }
function getHex(c: [number, number, number]) {
return `#${c.map(x => (x * 255).toString(16).padStart(2, '0')).join('')}`;
}
function getRgb(hex: string | number): [number, number, number] | null {
if (
typeof hex === 'number' ||
typeof hex !== 'string' ||
!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex)
) {
return null;
}
const m = hex.slice(1).match(/[0-9a-fA-F]{2}/g);
if (m == null) return [0, 0, 0];
return m.map(x => parseInt(x, 16) / 255) as [number, number, number];
}
definePage(() => ({ definePage(() => ({
title: 'Room', title: 'Room',
icon: 'ti ti-door', icon: 'ti ti-door',
@@ -554,8 +580,8 @@ definePage(() => ({
.overlayObjectInfoPanel { .overlayObjectInfoPanel {
position: absolute; position: absolute;
top: 8px; top: 16px;
right: 8px; right: 16px;
z-index: 1; z-index: 1;
padding: 16px; padding: 16px;
} }

View File

@@ -52,17 +52,27 @@ type RoomState = {
}; };
type RoomObjectInstance<Options> = { type RoomObjectInstance<Options> = {
onInited?: (room: RoomEngine, o: RoomStateObject<Options>, rootNode: BABYLON.Mesh) => void; onInited?: () => void;
onOptionsUpdated?: <K extends keyof Options, V extends Options[K]>(kv: [K, V]) => void;
interactions: Record<string, { interactions: Record<string, {
label: string; label: string;
fn: () => void; fn: () => void;
}>; }>;
primaryInteraction?: string | null; primaryInteraction?: string | null;
resetTemporaryState?: () => void;
dispose?: () => void; dispose?: () => void;
}; };
export const WORLD_SCALE = 100; export const WORLD_SCALE = 100;
type NumberOptionSchema = {
type: 'number';
label: string;
min?: number;
max?: number;
step?: number;
};
type ColorOptionSchema = { type ColorOptionSchema = {
type: 'color'; type: 'color';
label: string; label: string;
@@ -74,13 +84,13 @@ type SelectOptionSchema = {
enum: string[]; enum: string[];
}; };
type OptionsSchema = Record<string, ColorOptionSchema | SelectOptionSchema>; type OptionsSchema = Record<string, NumberOptionSchema | ColorOptionSchema | SelectOptionSchema>;
type GetOptionsSchemaValues<T extends OptionsSchema> = { type GetOptionsSchemaValues<T extends OptionsSchema> = {
[K in keyof T]: T[K] extends ColorOptionSchema ? [number, number, number] : T[K] extends SelectOptionSchema ? T[K]['enum'][number] : never; [K in keyof T]: T[K] extends NumberOptionSchema ? number : T[K] extends ColorOptionSchema ? [number, number, number] : T[K] extends SelectOptionSchema ? T[K]['enum'][number] : never;
}; };
type ObjectDef<OpSc extends OptionsSchema> = { type ObjectDef<OpSc extends OptionsSchema = OptionsSchema> = {
id: string; id: string;
name: string; name: string;
options: { options: {
@@ -92,7 +102,7 @@ type ObjectDef<OpSc extends OptionsSchema> = {
createInstance: (args: { createInstance: (args: {
room: RoomEngine; room: RoomEngine;
root: BABYLON.Mesh; root: BABYLON.Mesh;
options: GetOptionsSchemaValues<OpSc>; options: Readonly<GetOptionsSchemaValues<OpSc>>;
loaderResult: BABYLON.ISceneLoaderAsyncResult; loaderResult: BABYLON.ISceneLoaderAsyncResult;
meshUpdated: () => void; meshUpdated: () => void;
}) => RoomObjectInstance<GetOptionsSchemaValues<OpSc>>; }) => RoomObjectInstance<GetOptionsSchemaValues<OpSc>>;
@@ -176,7 +186,7 @@ export class RoomEngine {
objectMesh: BABYLON.Mesh; objectMesh: BABYLON.Mesh;
objectInstance: RoomObjectInstance<any>; objectInstance: RoomObjectInstance<any>;
objectState: RoomStateObject<any>; objectState: RoomStateObject<any>;
objectDef: ObjectDef<any>; objectDef: ObjectDef;
} | null>(null); } | null>(null);
private time: 0 | 1 | 2 = 0; // 0: 昼, 1: 夕, 2: 夜 private time: 0 | 1 | 2 = 0; // 0: 昼, 1: 夕, 2: 夜
private roomCollisionMeshes: BABYLON.AbstractMesh[] = []; private roomCollisionMeshes: BABYLON.AbstractMesh[] = [];
@@ -207,7 +217,7 @@ export class RoomEngine {
registerBuiltInLoaders(); registerBuiltInLoaders();
this.engine = new BABYLON.Engine(options.canvas, false, { alpha: false }); this.engine = new BABYLON.Engine(options.canvas, false, { alpha: false, antialias: false });
this.scene = new BABYLON.Scene(this.engine); this.scene = new BABYLON.Scene(this.engine);
//this.scene.useRightHandedSystem = true; //this.scene.useRightHandedSystem = true;
@@ -403,6 +413,14 @@ export class RoomEngine {
this.zGridPreviewPlane.isPickable = false; this.zGridPreviewPlane.isPickable = false;
this.zGridPreviewPlane.isVisible = false; this.zGridPreviewPlane.isVisible = false;
watch(this.isEditMode, (v) => {
if (v) {
for (const obji of this.objectInstances.values()) {
obji.resetTemporaryState?.();
}
}
});
let isDragging = false; let isDragging = false;
this.canvas.addEventListener('pointerdown', (ev) => { this.canvas.addEventListener('pointerdown', (ev) => {
@@ -1187,6 +1205,16 @@ export class RoomEngine {
this.grabbingCtx.rotation += delta; 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;
const obji = this.objectInstances.get(objectId);
if (obji == null) return;
obji.onOptionsUpdated?.([key, value]);
}
public resize() { public resize() {
this.engine.resize(); this.engine.resize();
} }

View File

@@ -5,16 +5,47 @@
import * as BABYLON from '@babylonjs/core'; import * as BABYLON from '@babylonjs/core';
import { defineObject, WORLD_SCALE } from '../engine.js'; import { defineObject, WORLD_SCALE } from '../engine.js';
import { createOverridedStates } from '../utility.js';
export const blind = defineObject({ export const blind = defineObject({
id: 'blind', id: 'blind',
defaultOptions: { name: 'Blind',
blades: 24, options: {
angle: 0, schema: {
open: 1, blades: {
type: 'number',
label: 'Number of blades',
min: 1,
max: 100,
},
angle: {
type: 'number',
label: 'Blade rotation angle (radian)',
min: -Math.PI / 2,
max: Math.PI / 2,
step: 0.01,
},
open: {
type: 'number',
label: 'Opening state',
min: 0,
max: 1,
step: 0.01,
},
},
default: {
blades: 24,
angle: 0,
open: 1,
},
}, },
placement: 'bottom', placement: 'bottom',
createInstance: ({ options, loaderResult, meshUpdated }) => { createInstance: ({ options, loaderResult, meshUpdated }) => {
const temp = createOverridedStates({
angle: () => options.angle,
open: () => options.open,
});
const blade = loaderResult.meshes[0].getChildMeshes().find(m => m.name === 'Blade') as BABYLON.Mesh; const blade = loaderResult.meshes[0].getChildMeshes().find(m => m.name === 'Blade') as BABYLON.Mesh;
blade.rotation = new BABYLON.Vector3(options.angle, 0, 0); blade.rotation = new BABYLON.Vector3(options.angle, 0, 0);
@@ -28,10 +59,10 @@ export const blind = defineObject({
for (let i = 0; i < options.blades; i++) { for (let i = 0; i < options.blades; i++) {
const b = blade.clone(); const b = blade.clone();
if (i / options.blades < options.open) { if (i / options.blades < temp.open) {
b.position.y -= (i * 4/*cm*/) / WORLD_SCALE; b.position.y -= (i * 4/*cm*/) / WORLD_SCALE;
} else { } else {
b.position.y -= (((options.blades - 1) * options.open * 4/*cm*/) + (i * 0.3/*cm*/)) / WORLD_SCALE; b.position.y -= (((options.blades - 1) * temp.open * 4/*cm*/) + (i * 0.3/*cm*/)) / WORLD_SCALE;
} }
blades.push(b); blades.push(b);
} }
@@ -41,7 +72,7 @@ export const blind = defineObject({
const applyAngle = () => { const applyAngle = () => {
for (const b of [blade, ...blades]) { for (const b of [blade, ...blades]) {
b.rotation.x = options.angle; b.rotation.x = temp.angle;
b.rotation.x += Math.random() * 0.3 - 0.15; b.rotation.x += Math.random() * 0.3 - 0.15;
} }
}; };
@@ -57,20 +88,25 @@ export const blind = defineObject({
adjustBladeRotation: { adjustBladeRotation: {
label: 'Adjust blade rotation', label: 'Adjust blade rotation',
fn: () => { fn: () => {
options.angle += Math.PI / 8; temp.angle += Math.PI / 8;
if (options.angle >= Math.PI / 2) options.angle = -Math.PI / 2; if (temp.angle >= Math.PI / 2) temp.angle = -Math.PI / 2;
applyAngle(); applyAngle();
}, },
}, },
openClose: { openClose: {
label: 'Open/close', label: 'Open/close',
fn: () => { fn: () => {
options.open -= 0.25; temp.open -= 0.25;
if (options.open < 0) options.open = 1; if (temp.open < 0) temp.open = 1;
applyOpeningState(); applyOpeningState();
}, },
}, },
}, },
resetTemporaryState: () => {
temp.$reset();
applyAngle();
applyOpeningState();
},
primaryInteraction: 'openClose', primaryInteraction: 'openClose',
}; };
}, },

View File

@@ -29,16 +29,21 @@ export const tabletopDigitalClock = defineObject({
}, },
placement: 'top', placement: 'top',
createInstance: ({ room, options, root }) => { createInstance: ({ room, options, root }) => {
const applyBodyColor = () => {
const bodyMesh = root.getChildMeshes().find(m => m.name.includes('__X_BODY__')) as BABYLON.Mesh;
const bodyMaterial = bodyMesh.material as BABYLON.PBRMaterial;
if (options.bodyStyle === 'color') {
const [r, g, b] = options.bodyColor;
bodyMaterial.albedoColor = new BABYLON.Color3(r, g, b);
} else {
bodyMaterial.albedoTexture = room.scene.getTextureByName('tabletop_digital_clock_wood');
}
};
return { return {
onInited: () => { onInited: () => {
const bodyMesh = root.getChildMeshes().find(m => m.name.includes('__X_BODY__')) as BABYLON.Mesh; applyBodyColor();
const bodyMaterial = bodyMesh.material as BABYLON.PBRMaterial;
if (options.bodyStyle === 'color') {
const [r, g, b] = options.bodyColor;
bodyMaterial.albedoColor = new BABYLON.Color3(r, g, b);
}
const segmentMeshes = { const segmentMeshes = {
'1a': root.getChildMeshes().find(m => m.name.includes('__TIME_7SEG_1A__')), '1a': root.getChildMeshes().find(m => m.name.includes('__TIME_7SEG_1A__')),
@@ -85,6 +90,11 @@ export const tabletopDigitalClock = defineObject({
} }
}, 1000)); }, 1000));
}, },
onOptionsUpdated: ([k, v]) => {
if (k === 'bodyColor') {
applyBodyColor();
}
},
interactions: {}, interactions: {},
}; };
}, },

View File

@@ -213,3 +213,28 @@ export function get7segMeshesOfCurrentTime(meshes: {
return result; return result;
} }
export function createOverridedStates<T extends Record<string, (() => any)>>(stateDefs: T): { [K in keyof T]: ReturnType<T[K]>; } & { $reset: () => void } {
const overridedStates = {} as { [K in keyof T]: ReturnType<T[K]>; };
const result = {} as { [K in keyof T]: ReturnType<T[K]>; } & { $reset: () => void };
for (const k in stateDefs) {
Object.defineProperty(result, k, {
get() {
return overridedStates[k] ?? stateDefs[k]();
},
set(value) {
overridedStates[k] = value;
},
enumerable: true,
});
}
result.$reset = () => {
for (const k in stateDefs) {
overridedStates[k] = stateDefs[k]();
}
};
return result;
}