From 17a3bdb5eb28e675cf401fda98de32fd286fe7be Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 19 Feb 2026 21:38:44 +0900
Subject: [PATCH] wip
---
packages/frontend/src/pages/room.vue | 44 +++++++++++---
packages/frontend/src/utility/room/engine.ts | 42 +++++++++++---
.../src/utility/room/objects/blind.ts | 58 +++++++++++++++----
.../room/objects/tabletopDigitalClock.ts | 26 ++++++---
packages/frontend/src/utility/room/utility.ts | 25 ++++++++
5 files changed, 160 insertions(+), 35 deletions(-)
diff --git a/packages/frontend/src/pages/room.vue b/packages/frontend/src/pages/room.vue
index 397687a422..549dc1c34d 100644
--- a/packages/frontend/src/pages/room.vue
+++ b/packages/frontend/src/pages/room.vue
@@ -26,14 +26,21 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ engine.selected.value.objectDef.name }}
-
-
-
+
+
+
+ Toggle Light
+ Edit mode: {{ engine.isEditMode.value ? 'on' : 'off' }}
+ addObject
+
@@ -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;
}
diff --git a/packages/frontend/src/utility/room/engine.ts b/packages/frontend/src/utility/room/engine.ts
index 379e89f2c2..b4b0ba07fc 100644
--- a/packages/frontend/src/utility/room/engine.ts
+++ b/packages/frontend/src/utility/room/engine.ts
@@ -52,17 +52,27 @@ type RoomState = {
};
type RoomObjectInstance = {
- onInited?: (room: RoomEngine, o: RoomStateObject, rootNode: BABYLON.Mesh) => void;
+ onInited?: () => void;
+ onOptionsUpdated?: (kv: [K, V]) => void;
interactions: Record 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;
+type OptionsSchema = Record;
type GetOptionsSchemaValues = {
- [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 = {
+type ObjectDef = {
id: string;
name: string;
options: {
@@ -92,7 +102,7 @@ type ObjectDef = {
createInstance: (args: {
room: RoomEngine;
root: BABYLON.Mesh;
- options: GetOptionsSchemaValues;
+ options: Readonly>;
loaderResult: BABYLON.ISceneLoaderAsyncResult;
meshUpdated: () => void;
}) => RoomObjectInstance>;
@@ -176,7 +186,7 @@ export class RoomEngine {
objectMesh: BABYLON.Mesh;
objectInstance: RoomObjectInstance;
objectState: RoomStateObject;
- objectDef: ObjectDef;
+ 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();
}
diff --git a/packages/frontend/src/utility/room/objects/blind.ts b/packages/frontend/src/utility/room/objects/blind.ts
index 0c2864e2f7..3db79467d4 100644
--- a/packages/frontend/src/utility/room/objects/blind.ts
+++ b/packages/frontend/src/utility/room/objects/blind.ts
@@ -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',
};
},
diff --git a/packages/frontend/src/utility/room/objects/tabletopDigitalClock.ts b/packages/frontend/src/utility/room/objects/tabletopDigitalClock.ts
index bbfb958075..74ec2f9772 100644
--- a/packages/frontend/src/utility/room/objects/tabletopDigitalClock.ts
+++ b/packages/frontend/src/utility/room/objects/tabletopDigitalClock.ts
@@ -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: {},
};
},
diff --git a/packages/frontend/src/utility/room/utility.ts b/packages/frontend/src/utility/room/utility.ts
index 0d45e95258..b96689e76c 100644
--- a/packages/frontend/src/utility/room/utility.ts
+++ b/packages/frontend/src/utility/room/utility.ts
@@ -213,3 +213,28 @@ export function get7segMeshesOfCurrentTime(meshes: {
return result;
}
+
+export function createOverridedStates any)>>(stateDefs: T): { [K in keyof T]: ReturnType; } & { $reset: () => void } {
+ const overridedStates = {} as { [K in keyof T]: ReturnType; };
+ const result = {} as { [K in keyof T]: ReturnType; } & { $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;
+}