Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 86 additions & 2 deletions src/components/content/GradientVectorScene.astro
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ const chartId = `gradient-${id || Math.random().toString(36).slice(2, 9)}`

<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { onHydration } from '../../utils/hydration'
import {
createBaseScene,
Expand All @@ -158,6 +159,9 @@ const chartId = `gradient-${id || Math.random().toString(36).slice(2, 9)}`
makeLabel,
getThemeColors3D,
setupZoomControls,
createTooltip,
showTooltip,
hideTooltip,
} from '../../utils/scene3d'

function hexToNum(hex: string): number {
Expand Down Expand Up @@ -208,7 +212,11 @@ const chartId = `gradient-${id || Math.random().toString(36).slice(2, 9)}`
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: any
let controls: OrbitControls
let pointsGroup: THREE.Group
let raycaster: THREE.Raycaster
let mouse: THREE.Vector2
let hoveredMesh: THREE.Mesh | null = null

function toWorld(v: { x: number; y: number; z: number }): THREE.Vector3 {
return new THREE.Vector3((v.x / axisRange) * CUBE, (v.y / axisRange) * CUBE, (v.z / axisRange) * CUBE)
Expand Down Expand Up @@ -267,14 +275,23 @@ const chartId = `gradient-${id || Math.random().toString(36).slice(2, 9)}`
rawPoints.forEach((p: any) => {
const color = p.color ? hexToNum(p.color) : 0xf59e0b
const pos = toWorld(p.position)
renderPoint(scene, pos, color, p.size || 10, 1)
const mesh = renderPoint(scene, pos, color, p.size || 10, 1)
mesh.userData = { index: rawPoints.indexOf(p), originalColor: new THREE.Color(color), data: p }
if (p.label) {
scene.add(
makeLabel(p.label, pos.clone().add(new THREE.Vector3(0, 0.18, 0)), color, 0.22, 0.09, 26),
)
}
})

pointsGroup = new THREE.Group()
scene.children.forEach((child) => {
if (child instanceof THREE.Mesh && child.geometry instanceof THREE.SphereGeometry) {
pointsGroup.add(child)
}
})
scene.add(pointsGroup)

rawVectors.forEach((v: any) => {
const color = v.color ? hexToNum(v.color) : 0x3b82f6
const vOrigin = v.origin ? toWorld(v.origin) : new THREE.Vector3(0, 0, 0)
Expand All @@ -295,6 +312,73 @@ const chartId = `gradient-${id || Math.random().toString(36).slice(2, 9)}`

setupZoomControls(wrapper as HTMLElement, camera, controls, DEFAULT_EYE, CENTER)

const tooltip = createTooltip(container)

raycaster = new THREE.Raycaster()
raycaster.params.Points = { threshold: 0.02 }
mouse = new THREE.Vector2()

renderer.domElement.addEventListener('mousemove', (e: MouseEvent) => {
const rect = renderer.domElement.getBoundingClientRect()
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1

raycaster.setFromCamera(mouse, camera)
const hits = raycaster.intersectObjects(pointsGroup.children, false)

if (hoveredMesh && (!hits.length || hits[0].object !== hoveredMesh)) {
;(hoveredMesh.material as THREE.MeshStandardMaterial).color.copy(
hoveredMesh.userData.originalColor,
)
;(hoveredMesh.material as THREE.MeshStandardMaterial).emissive.set(0x000000)
hoveredMesh = null
hideTooltip(tooltip)
}

renderer.domElement.style.cursor = hits.length ? 'pointer' : 'grab'

if (hits.length) {
const mesh = hits[0].object as THREE.Mesh
if (mesh !== hoveredMesh) {
hoveredMesh = mesh
;(mesh.material as THREE.MeshStandardMaterial).color.set(0xffffff)
;(mesh.material as THREE.MeshStandardMaterial).emissive.set(0x444444)
}
const d = mesh.userData.data
const lbl = d.label ? `<strong>${d.label}</strong><br/>` : ''
showTooltip(
tooltip,
e.offsetX,
e.offsetY,
`${lbl}x: ${d.position.x.toFixed(2)}<br/>y: ${d.position.y.toFixed(2)}<br/>z: ${d.position.z.toFixed(2)}`,
container.getBoundingClientRect(),
)
}
})

renderer.domElement.addEventListener('click', (e: MouseEvent) => {
const rect = renderer.domElement.getBoundingClientRect()
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1
raycaster.setFromCamera(mouse, camera)
const hits = raycaster.intersectObjects(pointsGroup.children, false)
if (hits.length) {
const d = (hits[0].object as THREE.Mesh).userData.data
container.dispatchEvent(
new CustomEvent('chart:click', {
bubbles: true,
detail: {
type: 'gradient',
x: d.position.x,
y: d.position.y,
z: d.position.z,
label: d.label,
},
}),
)
}
})

let rafId: number
function animate() {
rafId = requestAnimationFrame(animate)
Expand Down
88 changes: 86 additions & 2 deletions src/components/content/HyperplaneScene.astro
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ const chartId = `hyperplane-${id || Math.random().toString(36).slice(2, 9)}`

<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { onHydration } from '../../utils/hydration'
import {
createBaseScene,
Expand All @@ -142,6 +143,9 @@ const chartId = `hyperplane-${id || Math.random().toString(36).slice(2, 9)}`
makeLabel,
getThemeColors3D,
setupZoomControls,
createTooltip,
showTooltip,
hideTooltip,
} from '../../utils/scene3d'

function hexToNum(hex: string): number {
Expand Down Expand Up @@ -205,7 +209,11 @@ const chartId = `hyperplane-${id || Math.random().toString(36).slice(2, 9)}`
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: any
let controls: OrbitControls
let pointsGroup: THREE.Group
let raycaster: THREE.Raycaster
let mouse: THREE.Vector2
let hoveredMesh: THREE.Mesh | null = null

function scaleX(v: number) {
return ((v - xR.min) / (xR.max - xR.min)) * CUBE
Expand Down Expand Up @@ -267,14 +275,23 @@ const chartId = `hyperplane-${id || Math.random().toString(36).slice(2, 9)}`
const color = p.color ? hexToNum(p.color) : 0x3b82f6
const pos = toWorld(p.position)
pointsData.push(pos)
renderPoint(scene, pos, color, 10, 1)
const mesh = renderPoint(scene, pos, color, 10, 1)
mesh.userData = { index: rawPoints.indexOf(p), originalColor: new THREE.Color(color), data: p }
if (p.label) {
scene.add(
makeLabel(p.label, pos.clone().add(new THREE.Vector3(0, 0.15, 0)), color, 0.2, 0.08, 22),
)
}
})

pointsGroup = new THREE.Group()
scene.children.forEach((child) => {
if (child instanceof THREE.Mesh && child.geometry instanceof THREE.SphereGeometry) {
pointsGroup.add(child)
}
})
scene.add(pointsGroup)

if (showResiduals) {
pointsData.forEach((pos) => {
const projected = projectPointOnPlane(pos, planeNormalVec, planePointVec)
Expand All @@ -289,6 +306,73 @@ const chartId = `hyperplane-${id || Math.random().toString(36).slice(2, 9)}`

setupZoomControls(wrapper as HTMLElement, camera, controls, DEFAULT_EYE, CENTER)

const tooltip = createTooltip(container)

raycaster = new THREE.Raycaster()
raycaster.params.Points = { threshold: 0.02 }
mouse = new THREE.Vector2()

renderer.domElement.addEventListener('mousemove', (e: MouseEvent) => {
const rect = renderer.domElement.getBoundingClientRect()
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1

raycaster.setFromCamera(mouse, camera)
const hits = raycaster.intersectObjects(pointsGroup.children, false)

if (hoveredMesh && (!hits.length || hits[0].object !== hoveredMesh)) {
;(hoveredMesh.material as THREE.MeshStandardMaterial).color.copy(
hoveredMesh.userData.originalColor,
)
;(hoveredMesh.material as THREE.MeshStandardMaterial).emissive.set(0x000000)
hoveredMesh = null
hideTooltip(tooltip)
}

renderer.domElement.style.cursor = hits.length ? 'pointer' : 'grab'

if (hits.length) {
const mesh = hits[0].object as THREE.Mesh
if (mesh !== hoveredMesh) {
hoveredMesh = mesh
;(mesh.material as THREE.MeshStandardMaterial).color.set(0xffffff)
;(mesh.material as THREE.MeshStandardMaterial).emissive.set(0x444444)
}
const d = mesh.userData.data
const lbl = d.label ? `<strong>${d.label}</strong><br/>` : ''
showTooltip(
tooltip,
e.offsetX,
e.offsetY,
`${lbl}x: ${d.position.x.toFixed(2)}<br/>y: ${d.position.y.toFixed(2)}<br/>z: ${d.position.z.toFixed(2)}`,
container.getBoundingClientRect(),
)
}
})

renderer.domElement.addEventListener('click', (e: MouseEvent) => {
const rect = renderer.domElement.getBoundingClientRect()
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1
raycaster.setFromCamera(mouse, camera)
const hits = raycaster.intersectObjects(pointsGroup.children, false)
if (hits.length) {
const d = (hits[0].object as THREE.Mesh).userData.data
container.dispatchEvent(
new CustomEvent('chart:click', {
bubbles: true,
detail: {
type: 'hyperplane',
x: d.position.x,
y: d.position.y,
z: d.position.z,
label: d.label,
},
}),
)
}
})

let rafId: number
function animate() {
rafId = requestAnimationFrame(animate)
Expand Down
88 changes: 86 additions & 2 deletions src/components/content/Scene3D.astro
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ const chartId = `scene3d-${id || Math.random().toString(36).slice(2, 9)}`

<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { onHydration } from '../../utils/hydration'
import {
createBaseScene,
Expand All @@ -137,6 +138,9 @@ const chartId = `scene3d-${id || Math.random().toString(36).slice(2, 9)}`
makeLabel,
getThemeColors3D,
setupZoomControls,
createTooltip,
showTooltip,
hideTooltip,
} from '../../utils/scene3d'

function hexToNum(hex: string): number {
Expand Down Expand Up @@ -175,7 +179,11 @@ const chartId = `scene3d-${id || Math.random().toString(36).slice(2, 9)}`
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: any
let controls: OrbitControls
let pointsGroup: THREE.Group
let raycaster: THREE.Raycaster
let mouse: THREE.Vector2
let hoveredMesh: THREE.Mesh | null = null

function toWorld(v: { x: number; y: number; z: number }): THREE.Vector3 {
return new THREE.Vector3((v.x / axisRange) * CUBE, (v.y / axisRange) * CUBE, (v.z / axisRange) * CUBE)
Expand Down Expand Up @@ -212,14 +220,23 @@ const chartId = `scene3d-${id || Math.random().toString(36).slice(2, 9)}`
rawPoints.forEach((p: any) => {
const color = p.color ? hexToNum(p.color) : 0x3b82f6
const pos = toWorld(p.position)
renderPoint(scene, pos, color, p.size || 8, p.opacity ?? 1)
const mesh = renderPoint(scene, pos, color, p.size || 8, p.opacity ?? 1)
mesh.userData = { index: rawPoints.indexOf(p), originalColor: new THREE.Color(color), data: p }
if (p.label) {
scene.add(
makeLabel(p.label, pos.clone().add(new THREE.Vector3(0, 0.15, 0)), color, 0.2, 0.08, 22),
)
}
})

pointsGroup = new THREE.Group()
scene.children.forEach((child) => {
if (child instanceof THREE.Mesh && child.geometry instanceof THREE.SphereGeometry) {
pointsGroup.add(child)
}
})
scene.add(pointsGroup)

rawVectors.forEach((v: any) => {
const color = v.color ? hexToNum(v.color) : 0x22c55e
const origin = v.origin ? toWorld(v.origin) : new THREE.Vector3(0, 0, 0)
Expand Down Expand Up @@ -255,6 +272,73 @@ const chartId = `scene3d-${id || Math.random().toString(36).slice(2, 9)}`

setupZoomControls(wrapper as HTMLElement, camera, controls, DEFAULT_EYE, CENTER)

const tooltip = createTooltip(container)

raycaster = new THREE.Raycaster()
raycaster.params.Points = { threshold: 0.02 }
mouse = new THREE.Vector2()

renderer.domElement.addEventListener('mousemove', (e: MouseEvent) => {
const rect = renderer.domElement.getBoundingClientRect()
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1

raycaster.setFromCamera(mouse, camera)
const hits = raycaster.intersectObjects(pointsGroup.children, false)

if (hoveredMesh && (!hits.length || hits[0].object !== hoveredMesh)) {
;(hoveredMesh.material as THREE.MeshStandardMaterial).color.copy(
hoveredMesh.userData.originalColor,
)
;(hoveredMesh.material as THREE.MeshStandardMaterial).emissive.set(0x000000)
hoveredMesh = null
hideTooltip(tooltip)
}

renderer.domElement.style.cursor = hits.length ? 'pointer' : 'grab'

if (hits.length) {
const mesh = hits[0].object as THREE.Mesh
if (mesh !== hoveredMesh) {
hoveredMesh = mesh
;(mesh.material as THREE.MeshStandardMaterial).color.set(0xffffff)
;(mesh.material as THREE.MeshStandardMaterial).emissive.set(0x444444)
}
const d = mesh.userData.data
const lbl = d.label ? `<strong>${d.label}</strong><br/>` : ''
showTooltip(
tooltip,
e.offsetX,
e.offsetY,
`${lbl}x: ${d.position.x.toFixed(2)}<br/>y: ${d.position.y.toFixed(2)}<br/>z: ${d.position.z.toFixed(2)}`,
container.getBoundingClientRect(),
)
}
})

renderer.domElement.addEventListener('click', (e: MouseEvent) => {
const rect = renderer.domElement.getBoundingClientRect()
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1
raycaster.setFromCamera(mouse, camera)
const hits = raycaster.intersectObjects(pointsGroup.children, false)
if (hits.length) {
const d = (hits[0].object as THREE.Mesh).userData.data
container.dispatchEvent(
new CustomEvent('chart:click', {
bubbles: true,
detail: {
type: 'scene3d',
x: d.position.x,
y: d.position.y,
z: d.position.z,
label: d.label,
},
}),
)
}
})

let rafId: number
function animate() {
rafId = requestAnimationFrame(animate)
Expand Down
Loading
Loading