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 +
+
{{ s.label }}
+
+ +
+
+
+ +
+ + 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; +}