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">
{{ engine.selected.value.objectDef.name }}
</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 v-for="[k, s] in Object.entries(engine.selected.value.objectDef.options.schema)" :key="k">
<div>{{ s.label }}</div>
<div v-if="s.type === 'color'">
<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>
</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>
</template>
@@ -48,6 +55,7 @@ import { RoomEngine } from '@/utility/room/engine.js';
import { getObjectDef, OBJECT_DEFS } from '@/utility/room/object-defs.js';
import MkSelect from '@/components/MkSelect.vue';
import * as os from '@/os.js';
import MkInput from '@/components/MkInput.vue';
const canvas = useTemplateRef('canvas');
@@ -516,6 +524,24 @@ function addObject(ev: PointerEvent) {
})), 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(() => ({
title: 'Room',
icon: 'ti ti-door',
@@ -554,8 +580,8 @@ definePage(() => ({
.overlayObjectInfoPanel {
position: absolute;
top: 8px;
right: 8px;
top: 16px;
right: 16px;
z-index: 1;
padding: 16px;
}

View File

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

View File

@@ -5,16 +5,47 @@
import * as BABYLON from '@babylonjs/core';
import { defineObject, WORLD_SCALE } from '../engine.js';
import { createOverridedStates } from '../utility.js';
export const blind = defineObject({
id: 'blind',
defaultOptions: {
blades: 24,
angle: 0,
open: 1,
name: 'Blind',
options: {
schema: {
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',
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;
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++) {
const b = blade.clone();
if (i / options.blades < options.open) {
if (i / options.blades < temp.open) {
b.position.y -= (i * 4/*cm*/) / WORLD_SCALE;
} 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);
}
@@ -41,7 +72,7 @@ export const blind = defineObject({
const applyAngle = () => {
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;
}
};
@@ -57,20 +88,25 @@ export const blind = defineObject({
adjustBladeRotation: {
label: 'Adjust blade rotation',
fn: () => {
options.angle += Math.PI / 8;
if (options.angle >= Math.PI / 2) options.angle = -Math.PI / 2;
temp.angle += Math.PI / 8;
if (temp.angle >= Math.PI / 2) temp.angle = -Math.PI / 2;
applyAngle();
},
},
openClose: {
label: 'Open/close',
fn: () => {
options.open -= 0.25;
if (options.open < 0) options.open = 1;
temp.open -= 0.25;
if (temp.open < 0) temp.open = 1;
applyOpeningState();
},
},
},
resetTemporaryState: () => {
temp.$reset();
applyAngle();
applyOpeningState();
},
primaryInteraction: 'openClose',
};
},

View File

@@ -29,16 +29,21 @@ export const tabletopDigitalClock = defineObject({
},
placement: 'top',
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 {
onInited: () => {
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);
}
applyBodyColor();
const segmentMeshes = {
'1a': root.getChildMeshes().find(m => m.name.includes('__TIME_7SEG_1A__')),
@@ -85,6 +90,11 @@ export const tabletopDigitalClock = defineObject({
}
}, 1000));
},
onOptionsUpdated: ([k, v]) => {
if (k === 'bodyColor') {
applyBodyColor();
}
},
interactions: {},
};
},

View File

@@ -213,3 +213,28 @@ export function get7segMeshesOfCurrentTime(meshes: {
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;
}