From 76fe5c53f6356b6383d6261508dc069811675847 Mon Sep 17 00:00:00 2001 From: AmVoidGuy <132288247+AmVoidGuy@users.noreply.github.com> Date: Fri, 13 Jun 2025 22:33:46 -0400 Subject: [PATCH 1/5] feat: Face picking and camera movement --- src/graphics/Model.ts | 16 +++- src/jaged/JagEd.ts | 186 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 200 insertions(+), 2 deletions(-) diff --git a/src/graphics/Model.ts b/src/graphics/Model.ts index d6c9e33..dcf1ef3 100644 --- a/src/graphics/Model.ts +++ b/src/graphics/Model.ts @@ -1369,6 +1369,7 @@ export default class Model extends DoublyLinkable { this.originalVertexZ = new Int32Array(this.vertexZ); this.faceTextures = new Int32Array(this.faceCount); this.faceTextures.fill(-1); + this.initializeFaceTextures(); this.priorityVal = type.priorityVal; this.currentScaleX = 128; this.currentScaleY = 128; @@ -1378,6 +1379,19 @@ export default class Model extends DoublyLinkable { this.baseScaleZ = 128; } + private initializeFaceTextures(): void { + if (!this.faceInfo || !this.faceColor) { + return; + } + + for (let f = 0; f < this.faceCount; f++) { + const type = this.faceInfo[f] & 0x3; + if (type === 2 || type === 3) { + this.faceTextures[f] = this.faceColor[f]; + } + } + } + calculateBoundsCylinder(): void { this.maxY = 0; this.radius = 0; @@ -3428,7 +3442,7 @@ export default class Model extends DoublyLinkable { dz = z; vertexX[v] = x; - vertexY[v] = y; + vertexY[v] = -y; vertexZ[v] = z; } } diff --git a/src/jaged/JagEd.ts b/src/jaged/JagEd.ts index 2326c65..8f34631 100644 --- a/src/jaged/JagEd.ts +++ b/src/jaged/JagEd.ts @@ -8,6 +8,7 @@ import type Pix8 from '#/graphics/Pix8.ts'; import Jagfile from '#/io/Jagfile.ts'; import FileLoader from '#/jaged/FileLoader.ts'; import { downloadUrl, sleep } from '#/util/JsUtil.ts'; +import ColorConversion from '#/jaged/ColorConversion.ts'; const LocShapeSuffixMap = { _1: 0, @@ -135,6 +136,17 @@ export class JagEd extends GameShell { builtModel: Model | null = null; selectedModel: string | null = null; + moveSpeed: number = 5; + rotationSpeed: number = 2; + yaw: number = 0; + pitch: number = 0; + eyeX: number = 0; + eyeY: number = 0; + eyeZ: number = -420; + isDragging: boolean = false; + lastMouseX: number = 0; + lastMouseY: number = 0; + constructor() { super(true); @@ -182,12 +194,22 @@ export class JagEd extends GameShell { this.filterModelList() ); this.seqSearchInput.addEventListener("input", () => this.filterSeqList()); + this.setupMouseHandlers(); Pix3D.lowMemory = false; this.run(); } + private setupMouseHandlers(): void { + const canvasElement = document.getElementById('canvas') as HTMLCanvasElement; + if (canvasElement) { + canvasElement.addEventListener('mousedown', (e: MouseEvent) => this.handleMouseDown(e)); + canvasElement.addEventListener('mouseup', (e: MouseEvent) => this.handleMouseUp(e)); + canvasElement.addEventListener('mousemove', (e: MouseEvent) => this.handleMouseMove(e)); + } + } + async load(): Promise { const textures: Jagfile = await this.loadArchive('textures', 'textures', 30); @@ -209,7 +231,7 @@ export class JagEd extends GameShell { this.updateTextures(Pix3D.cycle); if (this.builtModel) { - this.builtModel.drawSimple(0, 0, 0, 0, 0, 0, -420); + this.builtModel.drawSimple(0, this.yaw, 0, this.pitch, this.eyeX, this.eyeY, this.eyeZ); } this.drawArea?.draw(0, 0); @@ -220,6 +242,163 @@ export class JagEd extends GameShell { async update(): Promise { this.sceneDelta++; + this.handleMovement(); + } + + private handleMouseDown(e: MouseEvent): void { + if (e.button === 0) { + if (this.builtModel && this.builtModel.pickedFace >= 0) { + this.displayFaceInfo(this.builtModel, this.builtModel.pickedFace); + } else { + this.hideFaceInfo(); + } + } else if (e.button === 2) { + this.isDragging = true; + this.lastMouseX = this.mouseX; + this.lastMouseY = this.mouseY; + const canvasElement = document.getElementById('canvas') as HTMLCanvasElement; + if (canvasElement) { + canvasElement.style.cursor = 'grabbing'; + } + } + } + + private handleMouseUp(e: MouseEvent): void { + if (e.button === 2) { + this.isDragging = false; + const canvasElement = document.getElementById('canvas') as HTMLCanvasElement; + if (canvasElement) { + canvasElement.style.cursor = 'default'; + } + } + } + + private handleMouseMove(e: MouseEvent): void { + if (this.isDragging) { + const deltaX: number = this.mouseX - this.lastMouseX; + const deltaY: number = this.mouseY - this.lastMouseY; + + this.yaw += deltaX * this.rotationSpeed; + this.pitch += deltaY * this.rotationSpeed; + + this.yaw = ((this.yaw % 2048) + 2048) % 2048; + this.pitch = ((this.pitch % 2048) + 2048) % 2048; + + this.lastMouseX = this.mouseX; + this.lastMouseY = this.mouseY; + } + } + + displayFaceInfo(model: any, faceIndex: number): void { + const faceInfoPanel = document.getElementById('face-info') as HTMLElement | null; + const faceDetails = document.getElementById('face-details') as HTMLElement | null; + + if (!faceInfoPanel || !faceDetails || !model) { + return; + } + + let html: string = ''; + + html += `
Face Index: ${faceIndex}
`; + + if (model.faceVertexA && model.faceVertexB && model.faceVertexC) { + const vertexA: number = model.faceVertexA[faceIndex]; + const vertexB: number = model.faceVertexB[faceIndex]; + const vertexC: number = model.faceVertexC[faceIndex]; + html += `
Vertices: ${vertexA}, ${vertexB}, ${vertexC}
`; + } + + let colorOrTextureInfo: string = ""; + const textureId: number | undefined = model.faceTextures?.[faceIndex]; + if (textureId !== undefined && textureId !== -1) { + const textureDisplayID: string = `ID: ${textureId}`; + colorOrTextureInfo = `
Texture: ${textureDisplayID}
`; + } else { + let colorValue: number | undefined = undefined; + let colorHex: string = "#ffffff"; + if (model.originalFaceColor && model.originalFaceColor[faceIndex] !== undefined) { + colorValue = model.originalFaceColor[faceIndex]; + } + if (colorValue !== undefined && Pix3D.hslPal && Pix3D.hslPal[colorValue] !== undefined) { + const paletteColor: number = Pix3D.hslPal[colorValue]; + const colorRgb = { + r: (paletteColor >> 16) & 0xff, + g: (paletteColor >> 8) & 0xff, + b: paletteColor & 0xff, + }; + colorHex = `#${colorRgb.r.toString(16).padStart(2, "0")}${colorRgb.g.toString(16).padStart(2, "0")}${colorRgb.b.toString(16).padStart(2, "0")}`; + const faceColorForHsl: number | undefined = model.originalFaceColor?.[faceIndex]; + if (faceColorForHsl !== undefined) { + colorOrTextureInfo = ` +
+ Source Color For Recol: ${ColorConversion.reverseHsl(faceColorForHsl)[0]} + +
`; + } else { + colorOrTextureInfo = `
Source Color For Recol: N/A (Invalid index for HSL)
`; + } + } else { + colorOrTextureInfo = `
Source Color For Recol: N/A
`; + } + } + + html += colorOrTextureInfo; + + if (model.facePriority && model.facePriority[faceIndex] !== undefined) { + html += `
Priority: ${model.facePriority[faceIndex]}
`; + } + + if (model.faceAlpha && model.faceAlpha[faceIndex] !== undefined) { + html += `
Alpha: ${model.faceAlpha[faceIndex]}
`; + } + + if (model.faceLabel && model.faceLabel[faceIndex] !== undefined) { + html += `
Label: ${model.faceLabel[faceIndex]}
`; + } + + faceDetails.innerHTML = html; + faceInfoPanel.style.display = 'block'; + } + + hideFaceInfo(): void { + const faceInfoPanel = document.getElementById('face-info') as HTMLElement | null; + if (faceInfoPanel) { + faceInfoPanel.style.display = 'none'; + } + } + + handleMovement(): void { + let moved: boolean = false; + + if (this.actionKey[87] || this.actionKey[119]) { + this.eyeY -= this.moveSpeed; + moved = true; + } + + if (this.actionKey[83] || this.actionKey[115]) { + this.eyeY += this.moveSpeed; + moved = true; + } + + if (this.actionKey[65] || this.actionKey[97]) { + this.eyeX -= this.moveSpeed; + moved = true; + } + + if (this.actionKey[68] || this.actionKey[100]) { + this.eyeX += this.moveSpeed; + moved = true; + } + + if (this.actionKey[81] || this.actionKey[113]) { + this.eyeZ -= this.moveSpeed; + moved = true; + } + + if (this.actionKey[69] || this.actionKey[101]) { + this.eyeZ += this.moveSpeed; + moved = true; + } } async showProgress(progress: number, message: string): Promise { @@ -2595,6 +2774,11 @@ export class JagEd extends GameShell { } showModel(modelId: string) { + this.eyeX = 0; + this.eyeY = 0; + this.eyeZ = -420; + this.yaw = 0; + this.pitch = 0; this.selectedModel = modelId; } From 22b0ce919de10f5fd88d0120bc8d6f01f2e4007a Mon Sep 17 00:00:00 2001 From: AmVoidGuy <132288247+AmVoidGuy@users.noreply.github.com> Date: Fri, 13 Jun 2025 22:45:49 -0400 Subject: [PATCH 2/5] chore: Removing unused var --- src/jaged/JagEd.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/jaged/JagEd.ts b/src/jaged/JagEd.ts index 8f34631..b44cd5d 100644 --- a/src/jaged/JagEd.ts +++ b/src/jaged/JagEd.ts @@ -368,36 +368,28 @@ export class JagEd extends GameShell { } handleMovement(): void { - let moved: boolean = false; - if (this.actionKey[87] || this.actionKey[119]) { this.eyeY -= this.moveSpeed; - moved = true; } if (this.actionKey[83] || this.actionKey[115]) { this.eyeY += this.moveSpeed; - moved = true; } if (this.actionKey[65] || this.actionKey[97]) { this.eyeX -= this.moveSpeed; - moved = true; } if (this.actionKey[68] || this.actionKey[100]) { this.eyeX += this.moveSpeed; - moved = true; } if (this.actionKey[81] || this.actionKey[113]) { this.eyeZ -= this.moveSpeed; - moved = true; } if (this.actionKey[69] || this.actionKey[101]) { this.eyeZ += this.moveSpeed; - moved = true; } } From 511f9b219cc5bdbebf82bafc7e384625dfddcf44 Mon Sep 17 00:00:00 2001 From: AmVoidGuy <132288247+AmVoidGuy@users.noreply.github.com> Date: Fri, 13 Jun 2025 23:18:34 -0400 Subject: [PATCH 3/5] fix: Changing to draw instead of drawsimple for better rendering --- src/graphics/Model.ts | 2 +- src/jaged/JagEd.ts | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/graphics/Model.ts b/src/graphics/Model.ts index dcf1ef3..1680935 100644 --- a/src/graphics/Model.ts +++ b/src/graphics/Model.ts @@ -3442,7 +3442,7 @@ export default class Model extends DoublyLinkable { dz = z; vertexX[v] = x; - vertexY[v] = -y; + vertexY[v] = y; vertexZ[v] = z; } } diff --git a/src/jaged/JagEd.ts b/src/jaged/JagEd.ts index b44cd5d..3824fb3 100644 --- a/src/jaged/JagEd.ts +++ b/src/jaged/JagEd.ts @@ -230,9 +230,18 @@ export class JagEd extends GameShell { Pix2D.clear(0x3F3F3F); this.updateTextures(Pix3D.cycle); - if (this.builtModel) { - this.builtModel.drawSimple(0, this.yaw, 0, this.pitch, this.eyeX, this.eyeY, this.eyeZ); - } + if (this.builtModel) { + const sinEyePitch = Pix3D.sin[this.pitch]; + const cosEyePitch = Pix3D.cos[this.pitch]; + const sinEyeYaw = Pix3D.sin[this.yaw]; + const cosEyeYaw = Pix3D.cos[this.yaw]; + + const relativeX = -this.eyeX; + const relativeY = -this.eyeY; + const relativeZ = -this.eyeZ; + + this.builtModel.draw(0, sinEyePitch, cosEyePitch, sinEyeYaw, cosEyeYaw, relativeX, relativeY, relativeZ, 0); + } this.drawArea?.draw(0, 0); } @@ -1146,7 +1155,7 @@ export class JagEd extends GameShell { /\//g, "_" )}`; - this.builtModel = model; + this.builtModel = clonedModel; document .querySelectorAll(".model-item") From d766e49a76840ff61b73a5d22c939755aff1da79 Mon Sep 17 00:00:00 2001 From: AmVoidGuy <132288247+AmVoidGuy@users.noreply.github.com> Date: Thu, 19 Jun 2025 22:33:24 -0400 Subject: [PATCH 4/5] feat: Catching up feature-wise to main repo --- src/graphics/Model.ts | 104 +++- src/jaged/JagEd.ts | 1370 +++++++++++++++++++++++++---------------- 2 files changed, 921 insertions(+), 553 deletions(-) diff --git a/src/graphics/Model.ts b/src/graphics/Model.ts index 1680935..e2c479c 100644 --- a/src/graphics/Model.ts +++ b/src/graphics/Model.ts @@ -146,6 +146,11 @@ export default class Model extends DoublyLinkable { static pickedCount: number = 0; static picked: Int32Array = new Int32Array(1000); static checkHoverFace: boolean = false; + public wireFrame: boolean = false; + public isHighlightedFace?: (faceIndex: number) => boolean; + private originalFaceColorA?: Int32Array; + private originalFaceColorB?: Int32Array; + private originalFaceColorC?: Int32Array; static unpack(models: Jagfile): void { try { @@ -1311,8 +1316,8 @@ export default class Model extends DoublyLinkable { faceTextures: Int32Array; - private hadOriginalFaceLabels: boolean = false; - private hadOriginalVertexLabels: boolean = false; + public hadOriginalFaceLabels: boolean = false; + public hadOriginalVertexLabels: boolean = false; private hadOriginalFacePriorities: boolean = false; private hadOriginalFaceAlphas: boolean = false; private hadOriginalFaceInfos: boolean = false; @@ -1325,6 +1330,8 @@ export default class Model extends DoublyLinkable { private baseScaleX: number = 128; private baseScaleY: number = 128; private baseScaleZ: number = 128; + faceLabelForExport: Int32Array | undefined; + vertexLabelForExport: Int32Array | undefined; constructor(type: ModelType) { super(); @@ -1377,6 +1384,7 @@ export default class Model extends DoublyLinkable { this.baseScaleX = 128; this.baseScaleY = 128; this.baseScaleZ = 128; + this.wireFrame = false; } private initializeFaceTextures(): void { @@ -1499,6 +1507,41 @@ export default class Model extends DoublyLinkable { } } + public applyFaceHighlighting(): void { + if (!this.isHighlightedFace) return; + + if (!this.originalFaceColorA && this.faceColorA) { + this.originalFaceColorA = new Int32Array(this.faceColorA); + } + if (!this.originalFaceColorB && this.faceColorB) { + this.originalFaceColorB = new Int32Array(this.faceColorB); + } + if (!this.originalFaceColorC && this.faceColorC) { + this.originalFaceColorC = new Int32Array(this.faceColorC); + } + + for (let f = 0; f < this.faceCount; f++) { + if (this.isHighlightedFace(f)) { + const highlightColorIndex: number = 573; + if (this.faceColorA) this.faceColorA[f] = highlightColorIndex; + if (this.faceColorB) this.faceColorB[f] = highlightColorIndex; + if (this.faceColorC) this.faceColorC[f] = highlightColorIndex; + } + } + } + + public restoreFaceColors(): void { + if (this.originalFaceColorA && this.faceColorA) { + this.faceColorA.set(this.originalFaceColorA); + } + if (this.originalFaceColorB && this.faceColorB) { + this.faceColorB.set(this.originalFaceColorB); + } + if (this.originalFaceColorC && this.faceColorC) { + this.faceColorC.set(this.originalFaceColorC); + } + } + applyTransforms(primaryId: number, secondaryId: number, mask: Int32Array | null): void { if (primaryId === -1) { return; @@ -1802,7 +1845,7 @@ export default class Model extends DoublyLinkable { } } - this.faceColor = null; + //this.faceColor = null; } // todo: better name, Java relies on overloads @@ -1995,7 +2038,7 @@ export default class Model extends DoublyLinkable { clipped = true; } - if ((clipped || this.texturedFaceCount > 0) && Model.vertexViewSpaceX && Model.vertexViewSpaceY && Model.vertexViewSpaceZ) { + if (Model.vertexViewSpaceX && Model.vertexViewSpaceY && Model.vertexViewSpaceZ) { Model.vertexViewSpaceX[v] = x; Model.vertexViewSpaceY[v] = y; Model.vertexViewSpaceZ[v] = z; @@ -2004,7 +2047,7 @@ export default class Model extends DoublyLinkable { try { // try catch for example a model being drawn from 3d can crash like at baxtorian falls - this.draw2(clipped, picking, typecode); + this.draw2(clipped, picking, typecode, this.wireFrame); } catch (err) { // console.error(err); } @@ -3224,21 +3267,25 @@ export default class Model extends DoublyLinkable { } if (this.hadOriginalFaceLabels) { - let actualFaceLabels: Uint8Array; - if (this.faceLabel) { - actualFaceLabels = Uint8Array.from(this.faceLabel); + let actualFaceLabels = new Uint8Array(this.faceCount).fill(0); + + if ((this as any).faceLabelForExport instanceof Int32Array) { + const src = (this as any).faceLabelForExport as Int32Array; + console.log("Using faceLabelForExport in export:", src.slice(0, 20)); + for (let i = 0; i < this.faceCount && i < src.length; i++) { + actualFaceLabels[i] = src[i]; + } } else if (this.labelFaces) { - actualFaceLabels = new Uint8Array(this.faceCount).fill(0); for (let l = 0; l < this.labelFaces.length; l++) { const indices = this.labelFaces[l]; if (indices) { for (let i = 0; i < indices.length; i++) { - if (indices[i] < this.faceCount) actualFaceLabels[indices[i]] = l; + if (indices[i] < this.faceCount) { + actualFaceLabels[indices[i]] = l; + } } } } - } else { - actualFaceLabels = new Uint8Array(this.faceCount).fill(0); } dataBlocks.push(actualFaceLabels); } @@ -3250,25 +3297,27 @@ export default class Model extends DoublyLinkable { dataBlocks.push(faceInfosData); } - if (this.hadOriginalVertexLabels) { - let actualVertexLabels: Uint8Array; - if (this.vertexLabel) { - actualVertexLabels = Uint8Array.from(this.vertexLabel); - } else if (this.labelVertices) { - actualVertexLabels = new Uint8Array(this.vertexCount).fill(0); - for (let l = 0; l < this.labelVertices.length; l++) { - const indices = this.labelVertices[l]; - if (indices) { - for (let i = 0; i < indices.length; i++) { - if (indices[i] < this.vertexCount) - actualVertexLabels[indices[i]] = l; + if (this.hadOriginalVertexLabels) { + let actualVertexLabels = new Uint8Array(this.vertexCount).fill(0); + + if ((this as any).vertexLabelForExport instanceof Int32Array) { + const src = (this as any).vertexLabelForExport as Int32Array; + for (let i = 0; i < this.vertexCount && i < src.length; i++) { + actualVertexLabels[i] = src[i]; + } + } else if (this.labelVertices) { + for (let l = 0; l < this.labelVertices.length; l++) { + const indices = this.labelVertices[l]; + if (indices) { + for (let i = 0; i < indices.length; i++) { + if (indices[i] < this.vertexCount) { + actualVertexLabels[indices[i]] = l; } } } - } else { - actualVertexLabels = new Uint8Array(this.vertexCount).fill(0); } - dataBlocks.push(actualVertexLabels); + } + dataBlocks.push(actualVertexLabels); } if (this.hadOriginalFaceAlphas) { @@ -3664,6 +3713,7 @@ export default class Model extends DoublyLinkable { newModel.baseScaleX = this.baseScaleX; newModel.baseScaleY = this.baseScaleY; newModel.baseScaleZ = this.baseScaleZ; + newModel.wireFrame = this.wireFrame; if (this.partMapping) { newModel.partMapping = { diff --git a/src/jaged/JagEd.ts b/src/jaged/JagEd.ts index 3824fb3..cb3d2e5 100644 --- a/src/jaged/JagEd.ts +++ b/src/jaged/JagEd.ts @@ -129,6 +129,8 @@ export class JagEd extends GameShell { modelSearchInput: HTMLInputElement; seqSearchInput: HTMLInputElement; exportModelButton: HTMLButtonElement | null; + changeFaceLabels: HTMLInputElement | null; + changeVertexLabels: HTMLInputElement | null; sceneDelta: number = 0; textureBuffer: Int8Array = new Int8Array(16384); @@ -147,6 +149,18 @@ export class JagEd extends GameShell { lastMouseX: number = 0; lastMouseY: number = 0; + private isVertexEditMode: boolean = false; + private isDraggingVertex: boolean = false; + private selectedVertex: number = -1; + private vertexDragStartViewZ: number = 0; + private vertexDragStartModelPos: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 }; + private vertexDragStartScreenPos: { x: number; y: number } = { x: 0, y: 0 }; + + private highlightedFaces: Set = new Set(); + private highlightedVertices: Set = new Set(); + + private showVertexNumbers: boolean = false; + constructor() { super(true); @@ -174,6 +188,8 @@ export class JagEd extends GameShell { }; this.currentSelectedAnimFrameInstance = null; this.loopSequenceCheckbox = null; + this.changeFaceLabels = null; + this.changeVertexLabels = null; this.modelSearchInput = document.getElementById( "model-search" ) as HTMLInputElement; @@ -201,6 +217,195 @@ export class JagEd extends GameShell { this.run(); } + private toggleVertexEditMode(): void { + const editToggle = document.getElementById("edit-toggle") as HTMLButtonElement; + if (!editToggle) return; + + this.isVertexEditMode = !this.isVertexEditMode; + + if (this.isVertexEditMode) { + editToggle.textContent = "Disable Vertex Editing"; + editToggle.classList.add("active"); + this.enableVertexEditMode(); + } else { + editToggle.textContent = "Enable Vertex Editing"; + editToggle.classList.remove("active"); + this.disableVertexEditMode(); + } + + this.updateVertexLabelUIState(); + } + + private enableVertexEditMode(): void { + const canvasElement = document.getElementById('canvas') as HTMLCanvasElement; + if (canvasElement) { + canvasElement.classList.add('vertex-edit'); + } + } + + private disableVertexEditMode(): void { + this.isDraggingVertex = false; + this.selectedVertex = -1; + const canvasElement = document.getElementById('canvas') as HTMLCanvasElement; + if (canvasElement) { + canvasElement.classList.remove('vertex-edit'); + } + this.clearVertexHighlights(); + document.querySelectorAll("#vertex-label-list .label-item").forEach((el) => el.classList.remove("selected", "highlighted-vertex")); + document.querySelectorAll("#vertex-label-panel .label-control-btn").forEach((el) => el.classList.remove("active")); + } + + private pickVertex(mouseX: number, mouseY: number): number { + if (!this.builtModel || !Model.vertexScreenX || !Model.vertexScreenY) { + return -1; + } + + const pickRadius = 8; + let closestVertex = -1; + let closestDistance = pickRadius * pickRadius; + + for (let v = 0; v < this.builtModel.vertexCount; v++) { + const screenX = Model.vertexScreenX[v]; + const screenY = Model.vertexScreenY[v]; + + if (screenX === -5000) continue; + + const dx = mouseX - screenX; + const dy = mouseY - screenY; + const distanceSqr = dx * dx + dy * dy; + + if (distanceSqr < closestDistance) { + closestDistance = distanceSqr; + closestVertex = v; + } + } + + return closestVertex; + } + + dragVertex(mouseX: number, mouseY: number) { + if (this.selectedVertex < 0 || !this.builtModel) { + return; + } + + if (this.vertexDragStartViewZ < 50) { + return; + } + + const startScreenX = this.vertexDragStartScreenPos.x; + const startScreenY = this.vertexDragStartScreenPos.y; + + const screenDeltaX = mouseX - startScreenX; + const screenDeltaY = mouseY - startScreenY; + const deltaViewX = (screenDeltaX * this.vertexDragStartViewZ) / 512.0; + const deltaViewY = (screenDeltaY * this.vertexDragStartViewZ) / 512.0; + + const deltaModel = this.viewVectorToModelVector(deltaViewX, deltaViewY, 0); + + const newModelX = Math.round(this.vertexDragStartModelPos.x + deltaModel.x); + const newModelY = Math.round(this.vertexDragStartModelPos.y + deltaModel.y); + const newModelZ = Math.round(this.vertexDragStartModelPos.z + deltaModel.z); + + if (newModelX !== this.builtModel.vertexX[this.selectedVertex] || + newModelY !== this.builtModel.vertexY[this.selectedVertex] || + newModelZ !== this.builtModel.vertexZ[this.selectedVertex]) { + this.builtModel.vertexX[this.selectedVertex] = newModelX; + this.builtModel.vertexY[this.selectedVertex] = newModelY; + this.builtModel.vertexZ[this.selectedVertex] = newModelZ; + this.sceneDelta++; + } + } + + viewVectorToModelVector(vecX: number, vecY: number, vecZ: number): {x: number, y: number, z: number} { + const f_sinEyePitch = Pix3D.sin[this.pitch] / 65536; + const f_cosEyePitch = Pix3D.cos[this.pitch] / 65536; + const f_sinEyeYaw = Pix3D.sin[this.yaw] / 65536; + const f_cosEyeYaw = Pix3D.cos[this.yaw] / 65536; + + const intermediateY = vecY * f_cosEyePitch + vecZ * f_sinEyePitch; + const intermediateZ = vecZ * f_cosEyePitch - vecY * f_sinEyePitch; + + const modelX = vecX * f_cosEyeYaw - intermediateZ * f_sinEyeYaw; + const modelY = intermediateY; + const modelZ = vecX * f_sinEyeYaw + intermediateZ * f_cosEyeYaw; + + return { x: modelX, y: modelY, z: modelZ }; + } + + private drawVertexHighlights(): void { + if (!this.builtModel || !Model.vertexScreenX || !Model.vertexScreenY) { + return; + } + + const canvas = document.getElementById('canvas') as HTMLCanvasElement; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + if (this.isVertexEditMode) { + if (this.highlightedVertices.size > 0) { + ctx.fillStyle = 'rgba(255, 255, 0, 0.8)'; + this.highlightedVertices.forEach(vertexIndex => { + if (vertexIndex < this.builtModel!.vertexCount) { + const screenX = Model.vertexScreenX![vertexIndex]; + const screenY = Model.vertexScreenY![vertexIndex]; + + if (screenX > -5000 && screenY > -5000) { + ctx.beginPath(); + ctx.arc(screenX, screenY, 3, 0, 2 * Math.PI); + ctx.fill(); + } + } + }); + } else { + ctx.fillStyle = 'rgba(255, 255, 0, 0.8)'; + for (let v = 0; v < this.builtModel.vertexCount; v++) { + const screenX = Model.vertexScreenX[v]; + const screenY = Model.vertexScreenY[v]; + + if (screenX > -5000 && screenY > -5000) { + ctx.beginPath(); + ctx.arc(screenX, screenY, 3, 0, 2 * Math.PI); + ctx.fill(); + } + } + } + } + if (this.highlightedVertices.size > 0) { + ctx.fillStyle = 'rgba(0, 204, 102, 0.9)'; + this.highlightedVertices.forEach(vertexIndex => { + if (this.builtModel && Model.vertexScreenX && Model.vertexScreenY && vertexIndex < this.builtModel.vertexCount) { + const screenX = Model.vertexScreenX[vertexIndex]; + const screenY = Model.vertexScreenY[vertexIndex]; + + if (screenX > -5000 && screenY > -5000) { + ctx.beginPath(); + ctx.arc(screenX, screenY, 5, 0, 2 * Math.PI); + ctx.fill(); + } + } + }); + } + if (this.showVertexNumbers) { + ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; + ctx.font = '11px Arial'; + ctx.textAlign = 'center'; + ctx.strokeStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.lineWidth = 2; + + for (let v = 0; v < this.builtModel.vertexCount; v++) { + const screenX = Model.vertexScreenX[v]; + const screenY = Model.vertexScreenY[v]; + + if (screenX > -5000 && screenY > -5000) { + const text = v.toString(); + const textY = screenY - 10; + ctx.strokeText(text, screenX, textY); + ctx.fillText(text, screenX, textY); + } + } + } + } + private setupMouseHandlers(): void { const canvasElement = document.getElementById('canvas') as HTMLCanvasElement; if (canvasElement) { @@ -230,20 +435,24 @@ export class JagEd extends GameShell { Pix2D.clear(0x3F3F3F); this.updateTextures(Pix3D.cycle); - if (this.builtModel) { - const sinEyePitch = Pix3D.sin[this.pitch]; - const cosEyePitch = Pix3D.cos[this.pitch]; - const sinEyeYaw = Pix3D.sin[this.yaw]; - const cosEyeYaw = Pix3D.cos[this.yaw]; + if (this.builtModel) { + const sinEyePitch = Pix3D.sin[this.pitch]; + const cosEyePitch = Pix3D.cos[this.pitch]; + const sinEyeYaw = Pix3D.sin[this.yaw]; + const cosEyeYaw = Pix3D.cos[this.yaw]; - const relativeX = -this.eyeX; - const relativeY = -this.eyeY; - const relativeZ = -this.eyeZ; - - this.builtModel.draw(0, sinEyePitch, cosEyePitch, sinEyeYaw, cosEyeYaw, relativeX, relativeY, relativeZ, 0); - } + const relativeX = -this.eyeX; + const relativeY = -this.eyeY; + const relativeZ = -this.eyeZ; + + this.builtModel.draw(0, sinEyePitch, cosEyePitch, sinEyeYaw, cosEyeYaw, relativeX, relativeY, relativeZ, 0); + } this.drawArea?.draw(0, 0); + + if (this.isVertexEditMode || this.showVertexNumbers || this.highlightedVertices.size > 0) { + this.drawVertexHighlights(); + } } async refresh(): Promise { @@ -254,8 +463,39 @@ export class JagEd extends GameShell { this.handleMovement(); } - private handleMouseDown(e: MouseEvent): void { - if (e.button === 0) { + handleMouseDown(e: MouseEvent): void { + const canvasElement = e.target as HTMLCanvasElement; + const rect = canvasElement.getBoundingClientRect(); + const scaleX = canvasElement.width / rect.width; + const scaleY = canvasElement.height / rect.height; + const mouseX = (e.clientX - rect.left) * scaleX | 0; + const mouseY = (e.clientY - rect.top) * scaleY | 0; + + if (this.isVertexEditMode && e.button === 0) { + const pickedVertex = this.pickVertex(mouseX, mouseY); + if (pickedVertex >= 0) { + this.selectedVertex = pickedVertex; + this.isDraggingVertex = true; + + if (Model.vertexViewSpaceZ) { + this.vertexDragStartViewZ = Model.vertexViewSpaceZ[pickedVertex]; + } + + if (this.builtModel) { + this.vertexDragStartModelPos.x = this.builtModel.vertexX[pickedVertex]; + this.vertexDragStartModelPos.y = this.builtModel.vertexY[pickedVertex]; + this.vertexDragStartModelPos.z = this.builtModel.vertexZ[pickedVertex]; + } + + if (Model.vertexScreenX && Model.vertexScreenY) { + this.vertexDragStartScreenPos.x = Model.vertexScreenX[pickedVertex]; + this.vertexDragStartScreenPos.y = Model.vertexScreenY[pickedVertex]; + } + + e.preventDefault(); + return; + } + } else if (e.button === 0) { if (this.builtModel && this.builtModel.pickedFace >= 0) { this.displayFaceInfo(this.builtModel, this.builtModel.pickedFace); } else { @@ -263,16 +503,25 @@ export class JagEd extends GameShell { } } else if (e.button === 2) { this.isDragging = true; - this.lastMouseX = this.mouseX; - this.lastMouseY = this.mouseY; - const canvasElement = document.getElementById('canvas') as HTMLCanvasElement; - if (canvasElement) { - canvasElement.style.cursor = 'grabbing'; + this.lastMouseX = mouseX; + this.lastMouseY = mouseY; + const canvasElement2 = document.getElementById("canvas"); + if (canvasElement2) { + canvasElement2.style.cursor = "grabbing"; } } } private handleMouseUp(e: MouseEvent): void { + if (this.isVertexEditMode && e.button === 0 && this.isDraggingVertex && !this.isSequenceRunning()) { + this.isDraggingVertex = false; + this.selectedVertex = -1; + e.preventDefault(); + if (this.builtModel) { + this.builtModel.saveCurrentVerticesAsOriginal(); + } + return; + } if (e.button === 2) { this.isDragging = false; const canvasElement = document.getElementById('canvas') as HTMLCanvasElement; @@ -281,20 +530,29 @@ export class JagEd extends GameShell { } } } - private handleMouseMove(e: MouseEvent): void { + const canvasElement = e.target as HTMLCanvasElement; + const rect = canvasElement.getBoundingClientRect(); + const scaleX = canvasElement.width / rect.width; + const scaleY = canvasElement.height / rect.height; + const mouseX = (e.clientX - rect.left) * scaleX | 0; + const mouseY = (e.clientY - rect.top) * scaleY | 0; + + if (this.isVertexEditMode && this.isDraggingVertex) { + this.dragVertex(mouseX, mouseY); + e.preventDefault(); + return; + } + if (this.isDragging) { - const deltaX: number = this.mouseX - this.lastMouseX; - const deltaY: number = this.mouseY - this.lastMouseY; - + const deltaX: number = mouseX - this.lastMouseX; + const deltaY: number = mouseY - this.lastMouseY; this.yaw += deltaX * this.rotationSpeed; this.pitch += deltaY * this.rotationSpeed; - - this.yaw = ((this.yaw % 2048) + 2048) % 2048; - this.pitch = ((this.pitch % 2048) + 2048) % 2048; - - this.lastMouseX = this.mouseX; - this.lastMouseY = this.mouseY; + this.yaw = (this.yaw % 2048 + 2048) % 2048; + this.pitch = (this.pitch % 2048 + 2048) % 2048; + this.lastMouseX = mouseX; + this.lastMouseY = mouseY; } } @@ -551,13 +809,6 @@ export class JagEd extends GameShell { if (clearDetailsBtn.disabled) return; this.hideNewTransformForm(); this.clearTransformEditor(); - - // const renderer = this.viewer.getRenderer(); - // if (renderer) { - // renderer.clearSpecificVertexHighlights(); - // renderer.clearSpecificFaceHighlights(); - // } - this.currentSelectedAnimFrameInstance = null; this.updateExportFrameButtonState(); clearDetailsBtn.disabled = true; @@ -586,31 +837,35 @@ export class JagEd extends GameShell { } clearTransformEditor() { - if (this.activeTransformEditor.element) { - this.activeTransformEditor.element.remove(); + if (this.activeTransformEditor.element && this.activeTransformEditor.element.parentNode) { + this.activeTransformEditor.element.parentNode.removeChild(this.activeTransformEditor.element); } - this.activeTransformEditor = { - element: null, - animFrame: null, - transformIndex: -1, - parentElement: null, - }; + + if (this.activeTransformEditor.parentElement) { + this.activeTransformEditor.parentElement.style.backgroundColor = "#2a2a2a"; + } + + this.activeTransformEditor.element = null; + this.activeTransformEditor.animFrame = null; + this.activeTransformEditor.transformIndex = -1; + this.activeTransformEditor.parentElement = null; - const deleteTransformBtn = document.getElementById( - "delete-transform-btn" - ) as HTMLButtonElement; - deleteTransformBtn.disabled = true; + const deleteTransformBtn = document.getElementById("delete-transform-btn") as HTMLButtonElement; + if (deleteTransformBtn) deleteTransformBtn.disabled = true; + + this.clearTransformHighlights(); } - showTransformEditor( - animFrame: any, - transformIndex: number, - parentElement: HTMLElement - ) { + showTransformEditor(animFrame: any, transformIndex: number, parentElement: HTMLElement) { this.clearTransformEditor(); - const deleteTransformBtn = document.getElementById( - "delete-transform-btn" - ) as HTMLButtonElement; + + document.querySelectorAll('.transform-editor').forEach(editor => { + if (editor.parentNode) { + editor.parentNode.removeChild(editor); + } + }); + + const deleteTransformBtn = document.getElementById("delete-transform-btn") as HTMLButtonElement; const editorDiv = document.createElement("div"); editorDiv.className = "transform-editor"; @@ -630,104 +885,79 @@ export class JagEd extends GameShell { transformTypeName = this.getTransformTypeName(transformType); } - editorDiv.innerHTML = `

Edit Transform ${transformIndex + 1 - } (${transformTypeName})

`; + editorDiv.innerHTML = `

Edit Transform ${transformIndex + 1} (${transformTypeName})

`; const currentValues = { - x: - animFrame.x && transformIndex < animFrame.x.length - ? animFrame.x[transformIndex] - : 0, - y: - animFrame.y && transformIndex < animFrame.y.length - ? animFrame.y[transformIndex] - : 0, - z: - animFrame.z && transformIndex < animFrame.z.length - ? animFrame.z[transformIndex] - : 0, + x: animFrame.x && transformIndex < animFrame.x.length ? animFrame.x[transformIndex] : 0, + y: animFrame.y && transformIndex < animFrame.y.length ? animFrame.y[transformIndex] : 0, + z: animFrame.z && transformIndex < animFrame.z.length ? animFrame.z[transformIndex] : 0, }; - let axesToCreateInputsFor = ["x", "y", "z"]; - if (transformType === 5) { - axesToCreateInputsFor = ["x"]; - } + const useSliders = transformType === 2 || transformType === 5; + const maxValue = useSliders ? 255 : undefined; - axesToCreateInputsFor.forEach((axis) => { + ["x", "y", "z"].forEach((axis) => { const axisDiv = document.createElement("div"); const label = document.createElement("label"); - label.htmlFor = `transform-edit-${axis}-${transformIndex}`; - - if (transformType === 5 && axis === "x") { - label.textContent = `Alpha:`; - } else { - label.textContent = `${axis.toUpperCase()}:`; - } - - let inputElement: HTMLInputElement; - let valueDisplaySpan: HTMLSpanElement | undefined; - - if (transformType === 2 || (transformType === 5 && axis === "x")) { - inputElement = document.createElement("input"); - inputElement.type = "range"; - inputElement.min = "0"; - inputElement.max = "255"; - inputElement.className = "transform-slider"; - - valueDisplaySpan = document.createElement("span"); - valueDisplaySpan.className = "slider-value-display"; - valueDisplaySpan.textContent = - currentValues[axis as keyof typeof currentValues].toString(); - - inputElement.id = `transform-edit-${axis}-${transformIndex}`; - inputElement.dataset.axis = axis; - inputElement.value = - currentValues[axis as keyof typeof currentValues].toString(); - - inputElement.addEventListener("input", (event) => { - const newValue = parseInt( - (event.target as HTMLInputElement).value, - 10 - ); + label.textContent = `${axis.toUpperCase()}:`; + + if (useSliders) { + axisDiv.style.display = "flex"; + axisDiv.style.alignItems = "center"; + axisDiv.style.marginBottom = "6px"; + + const sliderElement = document.createElement("input"); + sliderElement.type = "range"; + sliderElement.className = "transform-slider"; + sliderElement.min = "0"; + sliderElement.max = maxValue!.toString(); + sliderElement.value = Math.max(0, Math.min(maxValue!, currentValues[axis as keyof typeof currentValues])).toString(); + + const valueDisplay = document.createElement("span"); + valueDisplay.className = "slider-value-display"; + valueDisplay.textContent = sliderElement.value; + + const updateValue = () => { + const newValue = parseInt(sliderElement.value, 10); + valueDisplay.textContent = newValue.toString(); + if (!isNaN(newValue)) { if (animFrame[axis] && transformIndex < animFrame[axis].length) { animFrame[axis][transformIndex] = newValue; + animFrame.isModified = true; } else { - console.warn( - `Attempted to update transform out of bounds: axis ${axis}, index ${transformIndex}` - ); + console.warn(`Attempted to update transform out of bounds: axis ${axis}, index ${transformIndex}`); } - valueDisplaySpan!.textContent = newValue.toString(); this.refreshActiveAnimFrameDisplay(); } - }); + }; + + sliderElement.addEventListener("input", updateValue); + sliderElement.addEventListener("change", updateValue); + + axisDiv.appendChild(label); + axisDiv.appendChild(sliderElement); + axisDiv.appendChild(valueDisplay); } else { - inputElement = document.createElement("input"); + const inputElement = document.createElement("input"); inputElement.type = "number"; - inputElement.id = `transform-edit-${axis}-${transformIndex}`; - inputElement.dataset.axis = axis; - inputElement.value = - currentValues[axis as keyof typeof currentValues].toString(); - - inputElement.addEventListener("input", (event) => { - const newValue = parseFloat((event.target as HTMLInputElement).value); + inputElement.value = currentValues[axis as keyof typeof currentValues].toString(); + + inputElement.addEventListener("input", () => { + const newValue = parseInt(inputElement.value, 10); if (!isNaN(newValue)) { if (animFrame[axis] && transformIndex < animFrame[axis].length) { animFrame[axis][transformIndex] = newValue; + animFrame.isModified = true; } else { - console.warn( - `Attempted to update transform out of bounds: axis ${axis}, index ${transformIndex}` - ); + console.warn(`Attempted to update transform out of bounds: axis ${axis}, index ${transformIndex}`); } this.refreshActiveAnimFrameDisplay(); } }); - } - axisDiv.appendChild(label); - axisDiv.appendChild(inputElement); - if (valueDisplaySpan) { - axisDiv.appendChild(valueDisplaySpan); + axisDiv.appendChild(label); + axisDiv.appendChild(inputElement); } editorDiv.appendChild(axisDiv); }); @@ -739,10 +969,7 @@ export class JagEd extends GameShell { parentElement: parentElement, }; - parentElement.parentNode!.insertBefore( - editorDiv, - parentElement.nextSibling - ); + parentElement.parentNode!.insertBefore(editorDiv, parentElement.nextSibling); deleteTransformBtn.disabled = false; } @@ -807,30 +1034,10 @@ export class JagEd extends GameShell { clearFramesBtn.addEventListener("click", () => { if (clearFramesBtn.disabled) return; - const selectedModelId = this.selectedModel; - - if (selectedModelId) { - const modelMeshes = this.builtModel; - if (modelMeshes) { - const meshArray = Array.isArray(modelMeshes) - ? modelMeshes - : [modelMeshes]; - let modelToReset = null; - - for (const mesh of meshArray) { - if (mesh && mesh.userData && mesh.userData.originalModel) { - modelToReset = mesh.userData.originalModel; - break; - } - } - - if (modelToReset) { - modelToReset.resetToOriginal(); - // renderer.updateMeshGeometry(); - // renderer.updateVertexVisuals(selectedModelId); - } - } + if (this.builtModel) { + this.builtModel.resetToOriginal(); } + this.currentSelectedAnimFrameInstance = null; this.updateAnimFrameDetailsUI(null); document @@ -893,33 +1100,35 @@ export class JagEd extends GameShell { }); vertexToggle.addEventListener("click", () => { - const isActive = false; // this.viewer.getRenderer().toggleVertexNumbers(); - vertexToggle.textContent = isActive + this.showVertexNumbers = !this.showVertexNumbers; + vertexToggle.textContent = this.showVertexNumbers ? "Hide Vertex Numbers" : "Show Vertex Numbers"; - vertexToggle.classList.toggle("active", isActive); + vertexToggle.classList.toggle("active", this.showVertexNumbers); + + this.sceneDelta++; }); wireframeToggle.addEventListener("click", () => { - const isActive = false; // this.viewer.getRenderer().toggleWireframe(); - wireframeToggle.textContent = isActive - ? "Hide Wireframe" - : "Show Wireframe"; - wireframeToggle.classList.toggle("active", isActive); + const isActive = wireframeToggle.classList.contains("active"); + const newState = !isActive; + + wireframeToggle.textContent = newState ? "Hide Wireframe" : "Show Wireframe"; + wireframeToggle.classList.toggle("active", newState); + + if (this.builtModel) { + this.builtModel.wireFrame = newState; + } }); editToggle.addEventListener("click", () => { - const isActive = false; // this.viewer.getRenderer().toggleEditMode(); - editToggle.textContent = isActive - ? "Disable Vertex Editing" - : "Enable Vertex Editing"; - editToggle.classList.toggle("active", isActive); - this.updateVertexLabelUIState(); + this.toggleVertexEditMode(); }); viewModeSelect.addEventListener("change", () => { this.updateModelListUI(); this.updateExportButtonState(); + this.updateLabelsEditBoxes(); }); this.exportModelButton.addEventListener("click", () => @@ -928,26 +1137,26 @@ export class JagEd extends GameShell { this.updateExportButtonState(); } + updateLabelsEditBoxes() { + const viewModeSelect = document.getElementById("view-mode-select") as HTMLSelectElement; + const selectedMode = viewModeSelect.value; + + const isNpcMode = selectedMode === "npcs"; + + if (this.changeVertexLabels) { + this.changeVertexLabels.disabled = isNpcMode; + this.changeVertexLabels.checked = isNpcMode ? false : this.changeVertexLabels.checked; + } + + if (this.changeFaceLabels) { + this.changeFaceLabels.disabled = isNpcMode; + this.changeFaceLabels.checked = isNpcMode ? false : this.changeFaceLabels.checked; + } + } + updateExportButtonState() { if (this.exportModelButton) { - const selectedModelId = this.selectedModel; - let modelInstanceExists = false; - - if (selectedModelId) { - const modelMeshes = this.builtModel; - if (modelMeshes) { - const meshArray = Array.isArray(modelMeshes) - ? modelMeshes - : [modelMeshes]; - modelInstanceExists = - meshArray.length > 0 && - meshArray[0] && - meshArray[0].userData && - meshArray[0].userData.originalModel; - } - } - - this.exportModelButton.disabled = !modelInstanceExists; + this.exportModelButton.disabled = !this.builtModel; } } @@ -959,6 +1168,14 @@ export class JagEd extends GameShell { '
No model loaded
'; (document.getElementById("clear-labels") as HTMLButtonElement).disabled = true; + this.changeFaceLabels = document.getElementById("change-face-labels") as HTMLInputElement; + this.changeFaceLabels?.addEventListener("change", () => { + const selectedModel = this.selectedModel; + if (selectedModel) { + this.updateFaceLabelUI(selectedModel); + } + this.updateLabelsEditBoxes(); + }) } initializeVertexLabelPanel() { @@ -970,6 +1187,93 @@ export class JagEd extends GameShell { ( document.getElementById("clear-vertex-labels") as HTMLButtonElement ).disabled = true; + this.changeVertexLabels = document.getElementById("change-vertex-labels") as HTMLInputElement; + this.changeVertexLabels?.addEventListener("change", () => { + const selectedModel = this.selectedModel; + if (selectedModel) { + this.updateVertexLabelUI(selectedModel); + } + this.updateLabelsEditBoxes(); + }) + } + + buildRemappedFaceArray(model: Model, map: Record): Int32Array { + const out = new Int32Array(model.faceCount).fill(0); + + if (model.labelFaces) { + model.labelFaces.forEach((faces, grp) => { + if (!faces) return; + const target = map[grp] ?? grp; + for (let i = 0; i < faces.length; i++) out[faces[i]] = target; + }); + } + + return out; + } + + buildRemappedVertexArray(model: Model, map: Record): Int32Array { + const out = new Int32Array(model.vertexCount).fill(0); + + if (model.labelVertices) { + model.labelVertices.forEach((verts, grp) => { + if (!verts) return; + const target = map[grp] ?? grp; + for (let i = 0; i < verts.length; i++) out[verts[i]] = target; + }); + } + + return out; + } + + applyCustomFaceLabels(model: Model) { + const mapping: Record = {}; + const labelItems = document.querySelectorAll("#label-list .label-item"); + + labelItems.forEach((item) => { + const labelText = item.querySelector("span")?.textContent; + const input = item.querySelector("input") as HTMLInputElement; + + if (!labelText || !input) return; + + const match = labelText.match(/Label\s+(\d+)/); + if (!match) return; + + const originalId = parseInt(match[1]); + const newId = parseInt(input.value); + + if (!isNaN(newId) && newId !== originalId) { + mapping[originalId] = newId; + } + }); + + model.faceLabelForExport = this.buildRemappedFaceArray(model, mapping); + model.hadOriginalFaceLabels = true; + } + + + applyCustomVertexLabels(model: Model) { + const mapping: Record = {}; + const labelItems = document.querySelectorAll("#vertex-label-list .label-item"); + + labelItems.forEach((item) => { + const labelText = item.querySelector("span")?.textContent; + const input = item.querySelector("input") as HTMLInputElement; + + if (!labelText || !input) return; + + const match = labelText.match(/Label\s+(\d+)/); + if (!match) return; + + const originalId = parseInt(match[1]); + const newId = parseInt(input.value); + + if (!isNaN(newId) && newId !== originalId) { + mapping[originalId] = newId; + } + }); + + model.vertexLabelForExport = this.buildRemappedVertexArray(model, mapping); + model.hadOriginalVertexLabels = true; } async updateModelListUI() { @@ -1011,6 +1315,8 @@ export class JagEd extends GameShell { this.handleClearSequence(); this.updateAnimationButtonStates(); this.updateExportButtonState(); + this.resetWireframeButtonState(); + this.setupModelHighlighting(); } catch (error) { item.classList.remove("loading"); item.textContent = `${modelId} (error)`; @@ -1073,6 +1379,8 @@ export class JagEd extends GameShell { this.handleClearSequence(); this.updateAnimationButtonStates(); this.updateExportButtonState(); + this.resetWireframeButtonState(); + this.setupModelHighlighting(); } catch (error: any) { console.error(`Error loading object ${npcId}:`, error); item.classList.remove("loading"); @@ -1168,6 +1476,8 @@ export class JagEd extends GameShell { this.handleClearSequence(); this.updateAnimationButtonStates(); this.updateExportButtonState(); + this.resetWireframeButtonState(); + this.setupModelHighlighting(); } catch (error: any) { console.error(`Error loading object ${objId}:`, error); item.classList.remove("loading"); @@ -1341,6 +1651,8 @@ export class JagEd extends GameShell { this.handleClearSequence(); this.updateAnimationButtonStates(); this.updateExportButtonState(); + this.resetWireframeButtonState(); + this.setupModelHighlighting(); } catch (error: any) { console.error(`Error loading location ${locId}:`, error); item.classList.remove("loading"); @@ -1364,6 +1676,14 @@ export class JagEd extends GameShell { this.updateExportButtonState(); } + private resetWireframeButtonState(): void { + const wireframeToggle = document.getElementById("wireframe-toggle") as HTMLButtonElement; + if (wireframeToggle) { + wireframeToggle.classList.remove("active"); + wireframeToggle.textContent = "Show Wireframe"; + } + } + updateExportFrameButtonState() { const exportBtn = document.getElementById( "export-frame-btn" @@ -1429,8 +1749,6 @@ export class JagEd extends GameShell { if (status) { status.textContent = `Frame "${filename}" exported successfully.`; setTimeout(() => { - // const numModels = this.loader.getAvailableModels()?.length || 0; - // status.textContent = `Found ${numModels} .ob2 files`; }, 3000); } } catch (error: any) { @@ -1446,7 +1764,7 @@ export class JagEd extends GameShell { async handleExportModel() { const currentSelectedModel = this.selectedModel; - if (!this.viewer || !currentSelectedModel) { + if (!currentSelectedModel) { alert("No model selected to export."); this.updateExportButtonState(); return; @@ -1459,6 +1777,13 @@ export class JagEd extends GameShell { return; } + if (this.changeFaceLabels?.checked && modelInstance) { + this.applyCustomFaceLabels(modelInstance); + } + if (this.changeVertexLabels?.checked && modelInstance) { + this.applyCustomVertexLabels(modelInstance); + } + try { modelInstance.saveCurrentVerticesAsOriginal(); if (modelInstance.partMapping && modelInstance.partMapping.isNpcModel) { @@ -1577,22 +1902,7 @@ export class JagEd extends GameShell { "clear-seq" ) as HTMLButtonElement; - const selectedModelId = this.selectedModel; - let modelInstanceExists = false; - - if (selectedModelId) { - const selectedModelMeshes = this.builtModel; - if (selectedModelMeshes) { - const meshArray = Array.isArray(selectedModelMeshes) - ? selectedModelMeshes - : [selectedModelMeshes]; - modelInstanceExists = - meshArray.length > 0 && - meshArray[0] && - meshArray[0].userData && - meshArray[0].userData.originalModel; - } - } + const modelInstanceExists = !!this.builtModel; const selectedSeqItem = document.querySelector( "#seq-list .label-item.selected" @@ -1613,37 +1923,17 @@ export class JagEd extends GameShell { this.handleClearSequence(); } - // const renderer = this.viewer.getRenderer(); - const selectedModelId = this.selectedModel; - if (!selectedModelId) { - this.updateAnimationButtonStates(); - return; - } - - const modelMeshes = this.builtModel; - if (!modelMeshes) { + if (!this.selectedModel) { this.updateAnimationButtonStates(); return; } - const meshArray = Array.isArray(modelMeshes) ? modelMeshes : [modelMeshes]; - let modelToAnimate = null; - - for (const mesh of meshArray) { - if (mesh && mesh.userData && mesh.userData.originalModel) { - modelToAnimate = mesh.userData.originalModel; - break; - } - } - - if (!modelToAnimate) { + if (!this.builtModel) { this.updateAnimationButtonStates(); return; } - const selectedSeqItem = document.querySelector( - "#seq-list .label-item.selected" - ); + const selectedSeqItem = document.querySelector("#seq-list .label-item.selected"); if (!selectedSeqItem) { this.updateAnimationButtonStates(); return; @@ -1657,7 +1947,7 @@ export class JagEd extends GameShell { return; } - this.currentAnimation.modelRef = modelToAnimate; + this.currentAnimation.modelRef = this.builtModel; this.currentAnimation.seqId = seqId; this.currentAnimation.seqData = seqData; this.currentAnimation.frameIndex = 0; @@ -1720,12 +2010,6 @@ export class JagEd extends GameShell { if (numericFrameId !== -1) { model.resetToOriginal(); model.applyTransform(numericFrameId); - // this.viewer.getRenderer().updateMeshGeometry(); - - // const currentSelectedModelId = this.viewer.getRenderer().selectedModel; - // if (currentSelectedModelId) { - // this.viewer.getRenderer().updateVertexVisuals(currentSelectedModelId); - // } } this.currentAnimation.frameIndex = frameIndex + 1; @@ -1782,12 +2066,6 @@ export class JagEd extends GameShell { } if (this.currentAnimation.modelRef) { this.currentAnimation.modelRef.resetToOriginal(); - // this.viewer.getRenderer().updateMeshGeometry(); - - // const currentSelectedModelId = this.viewer.getRenderer().selectedModel; - // if (currentSelectedModelId) { - // this.viewer.getRenderer().updateVertexVisuals(currentSelectedModelId); - // } } this.currentAnimation = { @@ -1822,14 +2100,6 @@ export class JagEd extends GameShell { if (this.currentAnimation.modelRef) { if (this.currentAnimation.timerId) { this.currentAnimation.modelRef.resetToOriginal(); - // this.viewer.getRenderer().updateMeshGeometry(); - // const currentSelectedModelId = - // this.viewer.getRenderer().selectedModel; - // if (currentSelectedModelId) { - // this.viewer - // .getRenderer() - // .updateVertexVisuals(currentSelectedModelId); - // } } } this.currentAnimation = { @@ -2011,61 +2281,27 @@ export class JagEd extends GameShell { timerId: null, }; - // const renderer = this.viewer.getRenderer(); const selectedModelId = this.selectedModel; if (!selectedModelId) { this.updateAnimationButtonStates(); return; } - const modelMeshes = this.builtModel; - if (!modelMeshes) { - this.updateAnimationButtonStates(); - return; - } - - const meshArray = Array.isArray(modelMeshes) ? modelMeshes : [modelMeshes]; - let modelToAnimate: Model | null = null; - for (const mesh of meshArray) { - if (mesh && mesh.userData && mesh.userData.originalModel) { - modelToAnimate = mesh.userData.originalModel as Model; - break; - } - } - if (!modelToAnimate) { + if (!this.builtModel) { this.updateAnimationButtonStates(); return; } - this.currentAnimation.modelRef = modelToAnimate; + this.currentAnimation.modelRef = this.builtModel; - modelToAnimate.resetToOriginal(); - modelToAnimate.applyTransform(frameNumericId); - // renderer.updateMeshGeometry(); - // renderer.updateVertexVisuals(selectedModelId); + this.builtModel.resetToOriginal(); + this.builtModel.applyTransform(frameNumericId); this.updateAnimationButtonStates(); } - handleTransformOperationClick( - animFrame: AnimFrame, - transformIndexInFrame: number - ) { - // const renderer = this.viewer.getRenderer(); - const selectedModelId = this.selectedModel; - if (!selectedModelId) return; - - const modelMeshes = this.builtModel; // renderer.modelMeshes.get(selectedModelId); - if (!modelMeshes) return; - - const meshArray = Array.isArray(modelMeshes) ? modelMeshes : [modelMeshes]; - let modelInstance: Model | null = null; - for (const mesh of meshArray) { - if (mesh && mesh.userData && mesh.userData.originalModel) { - modelInstance = mesh.userData.originalModel as Model; - break; - } - } - if (!modelInstance) return; + handleTransformOperationClick(animFrame: AnimFrame, transformIndexInFrame: number) { + if (!this.selectedModel) return; + if (!this.builtModel) return; const animBase = animFrame.base; if ( @@ -2078,71 +2314,70 @@ export class JagEd extends GameShell { console.warn( "Cannot highlight: AnimFrame or AnimBase data incomplete or index out of bounds." ); - // renderer.clearSpecificVertexHighlights(); - // renderer.clearSpecificFaceHighlights(); + this.clearTransformHighlights(); return; } const baseGroupIndex = animFrame.bases[transformIndexInFrame]; - if ( - baseGroupIndex === undefined || - baseGroupIndex >= animBase.animTypes.length || - baseGroupIndex >= animBase.animLabels.length - ) { + + if (baseGroupIndex >= animBase.animLabels.length || baseGroupIndex >= animBase.animTypes.length) { console.warn( - `Invalid baseGroupIndex (${baseGroupIndex}) for transform. AnimBase might not have this group defined.` + `Base group index ${baseGroupIndex} is out of bounds for highlighting.` ); - // renderer.clearSpecificVertexHighlights(); - // renderer.clearSpecificFaceHighlights(); + this.clearTransformHighlights(); return; } - const transformType = animBase.animTypes[baseGroupIndex]; - const affectedModelLabels = animBase.animLabels[baseGroupIndex]; - - // renderer.clearSpecificVertexHighlights(); - // renderer.clearSpecificFaceHighlights(); + this.highlightAffectedModelParts(animBase, baseGroupIndex); + } - if (!affectedModelLabels || affectedModelLabels.length === 0) { - return; - } + private highlightTransformFaces(affectedLabels: Uint8Array): void { + if (!this.builtModel || !this.builtModel.labelFaces) return; - if (transformType === 5) { - const allFaceIndicesToHighlight = new Set(); - if (modelInstance.labelFaces) { - for (let i = 0; i < affectedModelLabels.length; i++) { - const faceGroupLabel = affectedModelLabels[i]; - if (modelInstance.labelFaces[faceGroupLabel]) { - const facesInGroup = modelInstance.labelFaces[faceGroupLabel]; - for (let j = 0; j < facesInGroup.length; j++) { - allFaceIndicesToHighlight.add(facesInGroup[j]); - } - } + const allFaceIndicesToHighlight = new Set(); + + for (let i = 0; i < affectedLabels.length; i++) { + const faceGroupLabel = affectedLabels[i]; + if (this.builtModel.labelFaces[faceGroupLabel]) { + const facesInGroup = this.builtModel.labelFaces[faceGroupLabel]; + for (let j = 0; j < facesInGroup.length; j++) { + allFaceIndicesToHighlight.add(facesInGroup[j]); } } - if (allFaceIndicesToHighlight.size > 0) { - // renderer.highlightSpecificFaces(Array.from(allFaceIndicesToHighlight)); - } - } else { - const allVertexIndicesToHighlight = new Set(); - if (modelInstance.labelVertices) { - for (let i = 0; i < affectedModelLabels.length; i++) { - const vertexGroupLabel = affectedModelLabels[i]; - if (modelInstance.labelVertices[vertexGroupLabel]) { - const verticesInGroup = - modelInstance.labelVertices[vertexGroupLabel]; - for (let j = 0; j < verticesInGroup.length; j++) { - allVertexIndicesToHighlight.add(verticesInGroup[j]); - } - } + } + + if (allFaceIndicesToHighlight.size > 0) { + this.highlightedFaces = allFaceIndicesToHighlight; + this.setupModelHighlighting(); + this.builtModel.applyFaceHighlighting(); + this.sceneDelta++; + } + } + + private highlightTransformVertices(affectedLabels: Uint8Array): void { + if (!this.builtModel || !this.builtModel.labelVertices) return; + + const allVertexIndicesToHighlight = new Set(); + + for (let i = 0; i < affectedLabels.length; i++) { + const vertexGroupLabel = affectedLabels[i]; + if (this.builtModel.labelVertices[vertexGroupLabel]) { + const verticesInGroup = this.builtModel.labelVertices[vertexGroupLabel]; + for (let j = 0; j < verticesInGroup.length; j++) { + allVertexIndicesToHighlight.add(verticesInGroup[j]); } } - if (allVertexIndicesToHighlight.size > 0) { - // renderer.highlightSpecificVertices( - // Array.from(allVertexIndicesToHighlight) - // ); - } } + + if (allVertexIndicesToHighlight.size > 0) { + this.highlightedVertices = allVertexIndicesToHighlight; + this.sceneDelta++; + } + } + + private clearTransformHighlights(): void { + this.clearFaceHighlights(); + this.clearVertexHighlights(); } updateAnimFrameDetailsUI(animFrame: AnimFrame | null) { @@ -2156,11 +2391,6 @@ export class JagEd extends GameShell { "add-new-transform-btn" ) as HTMLButtonElement; - // const renderer = this.viewer.getRenderer(); - // if (renderer) { - // renderer.clearSpecificVertexHighlights(); - // renderer.clearSpecificFaceHighlights(); - // } this.clearTransformEditor(); this.updateExportFrameButtonState(); @@ -2295,14 +2525,8 @@ export class JagEd extends GameShell { 10 ); if (!isNaN(transformIndex)) { - // const renderer = this.viewer.getRenderer(); - // if (renderer) { - // renderer.clearSpecificVertexHighlights(); - // renderer.clearSpecificFaceHighlights(); - // } - - this.handleTransformOperationClick(animFrame, transformIndex); this.showTransformEditor(animFrame, transformIndex, clickedElement); + this.handleTransformOperationClick(animFrame, transformIndex); const deleteTransformBtn = document.getElementById( "delete-transform-btn" @@ -2314,10 +2538,13 @@ export class JagEd extends GameShell { }); } + private isSequenceRunning(): boolean { + return this.currentAnimation.timerId !== null && + this.currentAnimation.modelRef !== null; + } + showNewTransformForm() { - const formContainer = document.getElementById( - "new-transform-form-container" - )!; + const formContainer = document.getElementById("new-transform-form-container")!; if ( !this.currentSelectedAnimFrameInstance || !this.currentSelectedAnimFrameInstance.base @@ -2339,17 +2566,19 @@ export class JagEd extends GameShell {
Select a base group to see affected model labels.
-
- - -
-
- - -
-
- - +
+
+ + +
+
+ + +
+
+ + +
@@ -2373,6 +2602,63 @@ export class JagEd extends GameShell { "affected-labels-info" ) as HTMLElement; + const updateInputTypes = (transformType: number) => { + const container = document.getElementById("new-transform-inputs-container")!; + const useSliders = transformType === 2 || transformType === 5; + const maxValue = useSliders ? 255 : undefined; + + if (useSliders) { + container.innerHTML = ` +
+ + + 0 +
+
+ + + 0 +
+
+ + + 0 +
+ `; + + ["x", "y", "z"].forEach(axis => { + const slider = document.getElementById(`new-transform-${axis}`) as HTMLInputElement; + const display = slider.nextElementSibling as HTMLSpanElement; + + const updateDisplay = () => { + display.textContent = slider.value; + }; + + slider.addEventListener("input", updateDisplay); + slider.addEventListener("change", updateDisplay); + }); + } else { + container.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+ `; + } + + this.activeNewTransformForm.xInput = document.getElementById("new-transform-x") as HTMLInputElement; + this.activeNewTransformForm.yInput = document.getElementById("new-transform-y") as HTMLInputElement; + this.activeNewTransformForm.zInput = document.getElementById("new-transform-z") as HTMLInputElement; + }; + if ( animBase.animLength > 0 && animBase.animTypes && @@ -2403,6 +2689,9 @@ export class JagEd extends GameShell { this.highlightAffectedModelParts(animBase, selectedGroupIndex); const transformType = animBase.animTypes[selectedGroupIndex]; + + updateInputTypes(transformType); + if ( this.activeNewTransformForm.xInput && this.activeNewTransformForm.yInput && @@ -2417,6 +2706,16 @@ export class JagEd extends GameShell { this.activeNewTransformForm.yInput.value = "0"; this.activeNewTransformForm.zInput.value = "0"; } + + if (transformType === 2 || transformType === 5) { + document.querySelectorAll("#new-transform-inputs-container .slider-value-display").forEach((display, index) => { + const values = ["0", "0", "0"]; + if (transformType === 3) { + values[0] = values[1] = values[2] = "128"; + } + (display as HTMLSpanElement).textContent = values[index]; + }); + } } } ); @@ -2439,16 +2738,12 @@ export class JagEd extends GameShell { } hideNewTransformForm() { - const formContainer = document.getElementById( - "new-transform-form-container" - )!; + const formContainer = document.getElementById("new-transform-form-container")!; formContainer.style.display = "none"; formContainer.innerHTML = ""; - // const renderer = this.viewer.getRenderer(); - // if (renderer) { - // renderer.clearSpecificVertexHighlights(); - // renderer.clearSpecificFaceHighlights(); - // } + + this.clearTransformHighlights(); + this.activeNewTransformForm = { baseGroupSelect: null, xInput: null, @@ -2493,19 +2788,10 @@ export class JagEd extends GameShell { console.warn( "AnimFrame ID is undefined, cannot refresh 3D model view after deletion. Resetting model." ); - // const renderer = this.viewer.getRenderer(); const selectedModelId = this.selectedModel; if (selectedModelId) { - const modelMeshes = this.builtModel; - if (modelMeshes) { - const meshArray = Array.isArray(modelMeshes) - ? modelMeshes - : [modelMeshes]; - if (meshArray[0] && meshArray[0].userData.originalModel) { - (meshArray[0].userData.originalModel as Model).resetToOriginal(); - // renderer.updateMeshGeometry(); - // renderer.updateVertexVisuals(selectedModelId); - } + if (this.builtModel) { + this.builtModel.resetToOriginal(); } } } @@ -2542,84 +2828,36 @@ export class JagEd extends GameShell { } highlightAffectedModelParts(animBase: AnimBase, baseGroupIndex: number) { - // const renderer = this.viewer.getRenderer(); - // const selectedModelId = renderer.selectedModel; - // if (!selectedModelId) return; - - // const modelMeshes = renderer.modelMeshes.get(selectedModelId); - // if (!modelMeshes) return; - - // const meshArray = Array.isArray(modelMeshes) ? modelMeshes : [modelMeshes]; - // let modelInstance: Model | null = null; - // for (const mesh of meshArray) { - // if (mesh && mesh.userData && mesh.userData.originalModel) { - // modelInstance = mesh.userData.originalModel as Model; - // break; - // } - // } - // if (!modelInstance) return; - - // if ( - // !animBase || - // !animBase.animLabels || - // !animBase.animTypes || - // baseGroupIndex >= animBase.animLabels.length || - // baseGroupIndex >= animBase.animTypes.length - // ) { - // console.warn( - // "Cannot highlight: AnimBase data incomplete or index out of bounds for highlighting." - // ); - // renderer.clearSpecificVertexHighlights(); - // renderer.clearSpecificFaceHighlights(); - // return; - // } - - // const transformType = animBase.animTypes[baseGroupIndex]; - // const affectedModelLabels = animBase.animLabels[baseGroupIndex]; - - // renderer.clearSpecificVertexHighlights(); - // renderer.clearSpecificFaceHighlights(); - - // if (!affectedModelLabels || affectedModelLabels.length === 0) { - // return; - // } - - // if (transformType === 5) { - // const allFaceIndicesToHighlight = new Set(); - // if (modelInstance.labelFaces) { - // for (let i = 0; i < affectedModelLabels.length; i++) { - // const faceGroupLabel = affectedModelLabels[i]; - // if (modelInstance.labelFaces[faceGroupLabel]) { - // const facesInGroup = modelInstance.labelFaces[faceGroupLabel]; - // for (let j = 0; j < facesInGroup.length; j++) { - // allFaceIndicesToHighlight.add(facesInGroup[j]); - // } - // } - // } - // } - // if (allFaceIndicesToHighlight.size > 0) { - // renderer.highlightSpecificFaces(Array.from(allFaceIndicesToHighlight)); - // } - // } else { - // const allVertexIndicesToHighlight = new Set(); - // if (modelInstance.labelVertices) { - // for (let i = 0; i < affectedModelLabels.length; i++) { - // const vertexGroupLabel = affectedModelLabels[i]; - // if (modelInstance.labelVertices[vertexGroupLabel]) { - // const verticesInGroup = - // modelInstance.labelVertices[vertexGroupLabel]; - // for (let j = 0; j < verticesInGroup.length; j++) { - // allVertexIndicesToHighlight.add(verticesInGroup[j]); - // } - // } - // } - // } - // if (allVertexIndicesToHighlight.size > 0) { - // renderer.highlightSpecificVertices( - // Array.from(allVertexIndicesToHighlight) - // ); - // } - // } + if (!this.builtModel) return; + + if ( + !animBase || + !animBase.animLabels || + !animBase.animTypes || + baseGroupIndex >= animBase.animLabels.length || + baseGroupIndex >= animBase.animTypes.length + ) { + console.warn( + "Cannot highlight: AnimBase data incomplete or index out of bounds for highlighting." + ); + this.clearTransformHighlights(); + return; + } + + const transformType = animBase.animTypes[baseGroupIndex]; + const affectedModelLabels = animBase.animLabels[baseGroupIndex]; + + this.clearTransformHighlights(); + + if (!affectedModelLabels || affectedModelLabels.length === 0) { + return; + } + + if (transformType === 5) { + this.highlightTransformFaces(affectedModelLabels); + } else { + this.highlightTransformVertices(affectedModelLabels); + } } handleConfirmAddNewTransform() { @@ -2664,70 +2902,150 @@ export class JagEd extends GameShell { } } - updateFaceLabelUI(modelId: string) { - const labelList = document.getElementById("label-list")!; + updateFaceLabelUI(modelId: string): void { + const labelList = document.getElementById("label-list") as HTMLElement; labelList.innerHTML = ""; - // const labels = this.viewer.getRenderer().getModelFaceLabels(modelId); - const clearBtn = document.getElementById( - "clear-labels" - ) as HTMLButtonElement; - - const modelIsLoaded = !!modelId; + + const clearBtn = document.getElementById("clear-labels") as HTMLButtonElement; + const modelIsLoaded = !!modelId && !!this.builtModel; if (clearBtn) clearBtn.disabled = !modelIsLoaded; - // if (!labels || labels.length === 0) { + if (!this.builtModel || !this.builtModel.labelFaces) { labelList.innerHTML = '
No face labels available
'; - // return; - // } - - // labels.forEach((label) => { - // const item = document.createElement("div"); - // item.className = "label-item"; - // item.innerHTML = `Label ${label.id}${label.faceCount} faces`; - // item.addEventListener("click", () => { - // document - // .querySelectorAll("#label-list .label-item") - // .forEach((el) => el.classList.remove("selected", "highlighted-face")); - // item.classList.add("highlighted-face"); - // this.viewer.getRenderer().highlightFaceLabel(label.id); - // }); - // labelList.appendChild(item); - // }); - } - - updateVertexLabelUI(modelId: string) { + return; + } + + this.builtModel.labelFaces.forEach((faceIndices, labelId) => { + if (faceIndices && faceIndices.length > 0) { + const item = document.createElement("div"); + item.className = "label-item"; + item.innerHTML = `Label ${labelId}${faceIndices.length} faces`; + + const input = document.createElement("input"); + input.type = "text"; + input.value = labelId.toString(); + input.className = "label-edit-input"; + input.style.marginLeft = "8px"; + input.style.width = "40px"; + input.disabled = !this.changeFaceLabels?.checked + + item.appendChild(input); + + item.addEventListener("click", () => { + document + .querySelectorAll("#label-list .label-item") + .forEach((el) => el.classList.remove("selected", "highlighted-face")); + item.classList.add("highlighted-face"); + this.highlightFaceLabel(labelId); + }); + labelList.appendChild(item); + } + }); + + if (labelList.children.length === 0) { + labelList.innerHTML = + '
No face labels available
'; + } + } + + updateVertexLabelUI(modelId: string): void { const list = document.getElementById("vertex-label-list") as HTMLElement; list.innerHTML = ""; - // const labels = this.viewer.getRenderer().getModelVertexLabels(modelId); + + this.updateVertexLabelUIState(); + + if (!this.builtModel || !this.builtModel.labelVertices) { + list.innerHTML = + '
No vertex labels available
'; + return; + } + + this.builtModel.labelVertices.forEach((vertexIndices, labelId) => { + if (vertexIndices && vertexIndices.length > 0) { + const item = document.createElement("div"); + item.className = "label-item"; + item.innerHTML = `Label ${labelId}${vertexIndices.length} vertices`; + + const input = document.createElement("input"); + input.type = "text"; + input.value = labelId.toString(); + input.className = "label-edit-input"; + input.style.marginLeft = "8px"; + input.style.width = "40px"; + input.disabled = !this.changeVertexLabels?.checked; + + item.appendChild(input); + + item.addEventListener("click", () => { + document + .querySelectorAll("#vertex-label-list .label-item") + .forEach((el) => + el.classList.remove("selected", "highlighted-vertex") + ); + item.classList.add("highlighted-vertex"); + this.highlightVertexLabel(labelId); + }); + list.appendChild(item); + } + }); - // if (!labels || labels.length === 0) { + if (list.children.length === 0) { list.innerHTML = '
No vertex labels available
'; - this.updateVertexLabelUIState(); - // return; - // } - // this.updateVertexLabelUIState(); - - // labels.forEach((label) => { - // const item = document.createElement("div"); - // item.className = "label-item"; - // item.innerHTML = `Label ${label.id}${label.vertexCount} vertices`; - // item.addEventListener("click", () => { - // if (!this.viewer.getRenderer().editMode) { - // alert("Enable Vertex Editing mode to highlight vertex labels."); - // return; - // } - // document - // .querySelectorAll("#vertex-label-list .label-item") - // .forEach((el) => - // el.classList.remove("selected", "highlighted-vertex") - // ); - // item.classList.add("highlighted-vertex"); - // this.viewer.getRenderer().highlightVertexLabel(label.id); - // }); - // list.appendChild(item); - // }); + } + } + + private highlightFaceLabel(labelId: number): void { + if (!this.builtModel || !this.builtModel.labelFaces || !this.builtModel.labelFaces[labelId]) { + return; + } + + this.clearFaceHighlights(); + + const faceIndices = this.builtModel.labelFaces[labelId]; + if (!faceIndices) return; + + this.highlightedFaces = new Set(faceIndices); + this.setupModelHighlighting(); + this.builtModel.applyFaceHighlighting(); + this.sceneDelta++; + } + + private setupModelHighlighting(): void { + if (this.builtModel) { + this.builtModel.isHighlightedFace = (faceIndex: number) => { + return this.highlightedFaces.has(faceIndex); + }; + } + } + + private highlightVertexLabel(labelId: number): void { + if (!this.builtModel || !this.builtModel.labelVertices || !this.builtModel.labelVertices[labelId]) { + return; + } + + this.clearVertexHighlights(); + + const vertexIndices = this.builtModel.labelVertices[labelId]; + if (!vertexIndices) return; + + this.highlightedVertices = new Set(vertexIndices); + + this.sceneDelta++; + } + + private clearFaceHighlights(): void { + if (this.builtModel) { + this.builtModel.restoreFaceColors(); + } + this.highlightedFaces = new Set(); + this.sceneDelta++; + } + + private clearVertexHighlights(): void { + this.highlightedVertices = new Set(); + this.sceneDelta++; } updateVertexLabelUIState() { @@ -2735,19 +3053,19 @@ export class JagEd extends GameShell { "clear-vertex-labels" ) as HTMLButtonElement; const modelLoaded = !!this.selectedModel; - const editModeActive = false; // this.viewer.getRenderer().editMode; + const editModeActive = this.isVertexEditMode; clearBtn.disabled = !modelLoaded || !editModeActive; } setupFaceLabelUI() { - const clearLabelsBtn = document.getElementById( - "clear-labels" - ) as HTMLButtonElement; + const clearLabelsBtn = document.getElementById("clear-labels") as HTMLButtonElement; clearLabelsBtn.addEventListener("click", () => { if (clearLabelsBtn.disabled) return; - // this.viewer.getRenderer().clearFaceHighlights(); + + this.clearFaceHighlights(); + document .querySelectorAll("#label-list .label-item") .forEach((el) => el.classList.remove("selected", "highlighted-face")); @@ -2758,13 +3076,13 @@ export class JagEd extends GameShell { } setupVertexLabelUI() { - const clearBtn = document.getElementById( - "clear-vertex-labels" - ) as HTMLButtonElement; + const clearBtn = document.getElementById("clear-vertex-labels") as HTMLButtonElement; clearBtn.addEventListener("click", () => { if (clearBtn.disabled) return; - // this.viewer.getRenderer().clearVertexHighlights(); + + this.clearVertexHighlights(); + document .querySelectorAll("#vertex-label-list .label-item") .forEach((el) => el.classList.remove("selected", "highlighted-vertex")); From b25b55adb07aceba2d26ac29fd8d8f9fee7e034e Mon Sep 17 00:00:00 2001 From: AmVoidGuy <132288247+AmVoidGuy@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:11:54 -0400 Subject: [PATCH 5/5] feat: animset compatability, 244-377 revisions. --- src/graphics/AnimBase.ts | 31 +++++++++++ src/graphics/AnimSet.ts | 111 +++++++++++++++++++++++++++++++++++++++ src/jaged/FileLoader.ts | 22 ++++++++ 3 files changed, 164 insertions(+) create mode 100644 src/graphics/AnimSet.ts diff --git a/src/graphics/AnimBase.ts b/src/graphics/AnimBase.ts index c4fa190..c5fc6ac 100644 --- a/src/graphics/AnimBase.ts +++ b/src/graphics/AnimBase.ts @@ -83,6 +83,37 @@ export default class AnimBase { return instance; } + static convertFromData377(id: number, fileData: Packet): AnimBase { + const instance = new AnimBase(); + instance.id = id; + + const length = fileData.g1(); + instance.animLength = length; + + const types = new Uint8Array(length); + const labels = new Array(length); + + for (let i = 0; i < length; i++) { + types[i] = fileData.g1(); + } + + for (let i = 0; i < length; i++) { + const labelCount = fileData.g1(); + const groupLabels = new Uint8Array(labelCount); + + for (let j = 0; j < labelCount; j++) { + groupLabels[j] = fileData.g1(); + } + labels[i] = groupLabels; + } + + instance.animTypes = types; + instance.animLabels = labels; + + AnimBase.instances[id] = instance; + return instance; + } + // ---- id: number = 0; diff --git a/src/graphics/AnimSet.ts b/src/graphics/AnimSet.ts new file mode 100644 index 0000000..8187061 --- /dev/null +++ b/src/graphics/AnimSet.ts @@ -0,0 +1,111 @@ +import AnimBase from "#/graphics/AnimBase.js"; +import AnimFrame from "#/graphics/AnimFrame.js"; +import Packet from '#/io/Packet.js'; + +export default class AnimSet { + static convertFromData(baseId: number, fileData: Uint8Array) { + const footer = new Packet(fileData); + footer.pos = fileData.length - 8; + const metaDataSize = footer.g2(); + const flagSize = footer.g2(); + const valuesSize = footer.g2(); + const delaysSize = footer.g2(); + + const metaData = new Packet(fileData); + metaData.pos = 0; + const frameCount = metaData.g2(); + + const flagData = new Packet(fileData); + flagData.pos = metaData.pos + metaDataSize; + + const valueData = new Packet(fileData); + valueData.pos = flagData.pos + flagSize; + + const delayData = new Packet(fileData); + delayData.pos = valueData.pos + valuesSize; + + const animBaseData = new Packet(fileData); + animBaseData.pos = delayData.pos + delaysSize; + + const animBase = AnimBase.convertFromData377(baseId, animBaseData); + + const tempGroups = new Array(500); + const tempX = new Array(500); + const tempY = new Array(500); + const tempZ = new Array(500); + + for (let i = 0; i < frameCount; i++) { + const frameIndex = metaData.g2(); + + const animFrame = new AnimFrame(); + + animFrame.id = frameIndex; + animFrame.frameDelay = delayData.g1(); + animFrame.base = animBase; + + const transformGroupCount = metaData.g1(); + let lastGroup = -1; + let tempIndex = 0; + + for (let j = 0; j < transformGroupCount; j++) { + const transformFlags = flagData.g1(); + + if (transformFlags > 0) { + if (animBase.animTypes![j] !== 0) { + for (let k = j - 1; k > lastGroup; k--) { + if (animBase.animTypes![k] === 0) { + tempGroups[tempIndex] = k; + tempX[tempIndex] = 0; + tempY[tempIndex] = 0; + tempZ[tempIndex] = 0; + tempIndex++; + break; + } + } + } + + tempGroups[tempIndex] = j; + let defaultValue = 0; + if (animBase.animTypes![tempGroups[tempIndex]] === 3) { + defaultValue = 128; + } + + if ((transformFlags & 0x1) == 0) { + tempX[tempIndex] = defaultValue; + } else { + tempX[tempIndex] = valueData.gsmart(); + } + + if ((transformFlags & 0x2) == 0) { + tempY[tempIndex] = defaultValue; + } else { + tempY[tempIndex] = valueData.gsmart(); + } + + if ((transformFlags & 0x4) == 0) { + tempZ[tempIndex] = defaultValue; + } else { + tempZ[tempIndex] = valueData.gsmart(); + } + + lastGroup = j; + tempIndex++; + } + } + + animFrame.frameLength = tempIndex; + animFrame.bases = new Int32Array(tempIndex); + animFrame.x = new Int32Array(tempIndex); + animFrame.y = new Int32Array(tempIndex); + animFrame.z = new Int32Array(tempIndex); + + for (let l = 0; l < tempIndex; l++) { + animFrame.bases[l] = tempGroups[l]; + animFrame.x[l] = tempX[l]; + animFrame.y[l] = tempY[l]; + animFrame.z[l] = tempZ[l]; + } + AnimFrame.instances[frameIndex] = animFrame; + } + } +} \ No newline at end of file diff --git a/src/jaged/FileLoader.ts b/src/jaged/FileLoader.ts index efd6f91..7dbb568 100644 --- a/src/jaged/FileLoader.ts +++ b/src/jaged/FileLoader.ts @@ -1,5 +1,6 @@ import AnimBase from '#/graphics/AnimBase.ts'; import AnimFrame from '#/graphics/AnimFrame.ts'; +import AnimSet from '#/graphics/AnimSet.ts'; import Model from '#/graphics/Model.ts'; import Packet from '#/io/Packet.ts'; import ColorConversion from '#/jaged/ColorConversion.ts'; @@ -923,6 +924,10 @@ export default class FileLoader { file.name.toLowerCase().endsWith(".seq") ); + const animsetFiles = Array.from(files).filter((file) => + file.name.toLowerCase().endsWith(".anim") + ); + const baseFiles = Array.from(files).filter((file) => file.name.toLowerCase().endsWith(".base") ); @@ -1029,6 +1034,17 @@ export default class FileLoader { } } + for (const file of animsetFiles) { + try { + const parts = file.name.split("_"); + const idString = parts[parts.length - 1]; + const currentId = parseInt(idString, 10); + await this.convertAnimset(currentId, file); + } catch (error) { + console.error(`Error processing Animset file ${file.name}:`, error); + } + } + for (const file of baseFiles) { try { const parts = file.name.split("_"); @@ -1107,6 +1123,12 @@ export default class FileLoader { return Model.convertFromData(data); } + async convertAnimset(currentId: number, file: File): Promise { + const arrayBuffer = await this.readFileAsArrayBuffer(file); + const uint8View = new Uint8Array(arrayBuffer); + AnimSet.convertFromData(currentId, uint8View); + } + async convertBase(currentId: number, file: File): Promise { const arrayBuffer = await this.readFileAsArrayBuffer(file); const uint8View = new Uint8Array(arrayBuffer);