// import { GUI } from 'dat.gui' import { ContextEventArgs, ContextRegistry, ContextEvent, showBalloonWarning, showBalloonMessage } from '@needle-tools/engine' import { addEventListener, notifyPropertyChanged, send } from './connection'; import { ClampToEdgeWrapping, LinearFilter, LinearMipMapLinearFilter, MirroredRepeatWrapping, NearestFilter, RepeatWrapping, sRGBEncoding, Texture, TextureLoader, Vector2 } from 'three'; import { EditorModification } from './types'; import { getIsTexture, getPropertyName } from './gltf_utils'; import { debug, getEditorId } from './common'; import { Registry } from './registry'; import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader"; import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'; import { createSceneCamera, onCameraChanged } from './scene_camera'; ContextRegistry.registerCallback(ContextEvent.ContextCreated, (args: ContextEventArgs) => { showBalloonMessage("🌵 Needle Editor has loaded") const id = getEditorId(); send("client:get-modifications-from-cache", { id }); }); let changeId = 0; export function notifySceneChanged() { const id = ++changeId; setTimeout(() => { if (id !== changeId) return; applyCached(); }, 10) } const messageCache: Map = new Map(); function applyCached() { for (const value of messageCache.values()) { onEditorModification(value, false); } } addEventListener("needle:editor:modified-property", async (data: EditorModification) => { onEditorModification(data, true); }) async function onEditorModification(data: EditorModification, allowCaching: boolean) { if (allowCaching) { const cache_key = data.guid + ":" + data.propertyName; messageCache.set(cache_key, data); } switch (data.guid) { case "scene-camera": onCameraChanged(data); return; } const target = Registry.get(data.guid); if (target == null || target === undefined || (Array.isArray(target) && target.length === 0)) { console.warn("Unknown object", data.guid, debug ? Registry.map : ""); showBalloonWarning("Editor: could not resolve object being edited"); return } if (debug) console.log("Modification", data, target); await preprocessModification(data); if (Array.isArray(target)) { for (const t of target) { applyModification(t, data); } } else { applyModification(target, data); } } const textureLoader = new TextureLoader(); const exrLoader = new EXRLoader(); const hdrLoader = new RGBELoader(); async function preprocessModification(data: EditorModification) { let value = data.value; if (value === null || value === undefined) { // if a value is null we want to set it to null (never to undefined since that property exists on the object) data.value = null; } else if (getIsTexture(data.propertyName, value)) { if (await tryLoadTexture(data) === false) { console.warn("Failed to load texture"); return; } } else if (data.propertyName === "normalScale" || data.propertyName === "normalTextureScale") { console.log(data.propertyName); if (typeof value === "number") { data.value = new Vector2(value, value); } } else if (data.propertyName === "fov") { data.propertyName = "fieldOfView"; } else if (typeof value === "object") { // traverse the object to e.g. handle textures (for example a sprite has a texture encoded inside) } } function applyModification(target: object, data: EditorModification) { let propertyName: string | null = data.propertyName; // If we get a color modification with an alpha value we want to set the opacity as well // We split the modification into two here and generate a new one for the opacity if (propertyName === "baseColorFactor" && data.value.a !== undefined) { const opacity = data.value.a; const mod: EditorModification = { guid: data.guid, propertyName: "opacity", value: opacity } applyModification(target, mod); } switch (propertyName) { case "component:added": console.log("Handle adding component", data); return; case "component:removed": console.log("Handle removing component", data); return; } let targetProperty = target[data.propertyName]; if (targetProperty === undefined) { propertyName = getPropertyName(data.propertyName, target, data)!; if (propertyName == null || propertyName == undefined) { return console.warn("Unknown property", data.propertyName); } targetProperty = target[propertyName]; if (targetProperty === undefined) { return console.warn("Unknown property", data.propertyName); } } const value = data.value; // textures are set to null below if (value === null) { targetProperty = null; } else if (targetProperty instanceof Texture) { targetProperty = value; } // if we modify e.g. a color we want to copy the values and not replace the original object else if (typeof targetProperty === "object" && targetProperty && typeof targetProperty["copy"] === "function") { targetProperty.copy(value); } else { // Make sure to not share arrays if (Array.isArray(value)) { targetProperty = [...value]; } else { targetProperty = value; } } // check if it can be assigned. in some cases we can not override the object // e.g. when modifying the position or rotation of an object // but in those cases we use the copy method from above to copy the values. let desc; let obj = target; let couldAssign = false; do { desc = Object.getOwnPropertyDescriptor(obj, propertyName); if (!desc) { obj = Object.getPrototypeOf(obj); if (!obj) break; } else if (desc.writable || desc.set) { couldAssign = true; target[propertyName] = targetProperty; } } while (!desc) if (debug && !couldAssign) { console.log("Could not assign", data.propertyName, target); } if ("needsUpdate" in target) { target.needsUpdate = true; } } async function tryLoadTexture(data: EditorModification) { const value = data.value; if (!(value instanceof Texture)) { let textureData = value; if (typeof value === "object" && value.data) { textureData = value.data; } let loader: any = null; if (textureData.startsWith("data:image/png") || textureData.startsWith("data:image/jpeg") || textureData.startsWith("data:image/jpg")) { loader = textureLoader; } if (textureData.startsWith("data:image/exr")) { loader = exrLoader; } else if (textureData.startsWith("data:image/hdr")) { loader = hdrLoader; } if (!loader) { let format = textureData as string; const formatEnd = format.indexOf(";"); if (formatEnd > 0) format = format.substring(0, formatEnd); if (format.startsWith("data:image/")) format = format.substring("data:image/".length); console.error("Unknown texture format:", format); data.value = null; return false; } data.value = await loader.loadAsync(textureData); const tex = data.value as Texture; if (tex) { tex.flipY = false; tex.encoding = sRGBEncoding; tex.needsUpdate = true; if (typeof value.filter === "number") { switch (value.filter) { case FilterMode.Point: tex.magFilter = NearestFilter; tex.minFilter = NearestFilter; break; case FilterMode.Bilinear: tex.magFilter = LinearFilter; tex.minFilter = LinearFilter; break; case FilterMode.Trilinear: tex.magFilter = LinearFilter; tex.minFilter = LinearMipMapLinearFilter; break; } } if (typeof value.name === "string") tex.name = value.name; if (typeof value.wrap === "number") { switch (value.wrap) { case TextureWrapMode.Repeat: tex.wrapS = RepeatWrapping; tex.wrapT = RepeatWrapping; break; case TextureWrapMode.Clamp: tex.wrapS = ClampToEdgeWrapping; tex.wrapT = ClampToEdgeWrapping; break; case TextureWrapMode.Mirror: case TextureWrapMode.MirrorOnce: tex.wrapS = MirroredRepeatWrapping; tex.wrapT = MirroredRepeatWrapping; break; } } if (typeof value.anisotropy === "number") tex.anisotropy = value.anisotropy; } return true; } return true; } enum FilterMode { Point = 0, Bilinear = 1, Trilinear = 2, } enum TextureWrapMode { Repeat = 0, Clamp = 1, Mirror = 2, MirrorOnce = 3, }