Finish virtual profile
This commit is contained in:
parent
46db218492
commit
913af8fba6
3 changed files with 298 additions and 132 deletions
|
@ -1,6 +1,14 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChangeEvent, ReactNode, useContext, useRef, useState } from "react";
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
ReactNode,
|
||||||
|
forwardRef,
|
||||||
|
useContext,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SceneViewContext,
|
SceneViewContext,
|
||||||
|
@ -15,7 +23,7 @@ function Toggle({
|
||||||
defaultChecked,
|
defaultChecked,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
onChange: (e: any) => void;
|
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||||
defaultChecked?: boolean;
|
defaultChecked?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
@ -35,29 +43,49 @@ function Toggle({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Accordion({
|
enum Position {
|
||||||
children,
|
Start,
|
||||||
title,
|
Center,
|
||||||
}: {
|
End,
|
||||||
children?: ReactNode;
|
}
|
||||||
|
|
||||||
|
interface AccordionRef {
|
||||||
|
open: (b: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccordionProps {
|
||||||
title: string;
|
title: string;
|
||||||
}) {
|
position: Position;
|
||||||
const [expanded, setExpanded] = useState<boolean>(true);
|
open: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Accordion = forwardRef<AccordionRef, AccordionProps>(
|
||||||
|
({ children, title, position, open }, ref) => {
|
||||||
|
const [expanded, setExpanded] = useState<boolean>(open);
|
||||||
const accordionBodyRef = useRef<HTMLDivElement>(null);
|
const accordionBodyRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
open: (b: boolean) => setExpanded(b),
|
||||||
|
}));
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
if (!accordionBodyRef.current) return;
|
if (!accordionBodyRef.current) return;
|
||||||
|
|
||||||
accordionBodyRef.current.classList.toggle("hidden");
|
|
||||||
setExpanded(!expanded);
|
setExpanded(!expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const className =
|
||||||
|
position === Position.Center
|
||||||
|
? "flex items-center justify-between w-full p-5 font-medium rtl:text-right text-gray-500 border border-b border-gray-200 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-800 dark:border-gray-700 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 gap-3 hover:cursor-pointer"
|
||||||
|
: "flex items-center justify-between w-full p-5 font-medium rtl:text-right text-gray-500 border border-b border-gray-200 rounded-t focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-800 dark:border-gray-700 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 gap-3 hover:cursor-pointer";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 id="accordion-collapse-heading-1">
|
<h2 id="accordion-collapse-heading-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center justify-between w-full p-5 font-medium rtl:text-right text-gray-500 border border-b-0 border-gray-200 rounded-t focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-800 dark:border-gray-700 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 gap-3 hover:cursor-pointer"
|
className={className}
|
||||||
data-accordion-target="#accordion-collapse-body-1"
|
data-accordion-target="#accordion-collapse-body-1"
|
||||||
aria-expanded={expanded ? "true" : "false"}
|
aria-expanded={expanded ? "true" : "false"}
|
||||||
aria-controls="accordion-collapse-body-1"
|
aria-controls="accordion-collapse-body-1"
|
||||||
|
@ -66,7 +94,9 @@ function Accordion({
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
<svg
|
<svg
|
||||||
data-accordion-icon
|
data-accordion-icon
|
||||||
className="w-3 h-3 rotate-180 shrink-0"
|
className={
|
||||||
|
expanded ? "w-3 h-3 shrink-0" : "w-3 h-3 rotate-180 shrink-0"
|
||||||
|
}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
@ -86,6 +116,7 @@ function Accordion({
|
||||||
id="accordion-collapse-body-1"
|
id="accordion-collapse-body-1"
|
||||||
ref={accordionBodyRef}
|
ref={accordionBodyRef}
|
||||||
aria-labelledby="accordion-collapse-heading-1"
|
aria-labelledby="accordion-collapse-heading-1"
|
||||||
|
className={expanded ? "" : "hidden"}
|
||||||
>
|
>
|
||||||
<div className="p-5 border border-gray-200 dark:border-gray-700 dark:bg-gray-900">
|
<div className="p-5 border border-gray-200 dark:border-gray-700 dark:bg-gray-900">
|
||||||
{children}
|
{children}
|
||||||
|
@ -94,9 +125,13 @@ function Accordion({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
Accordion.displayName = "Accordion";
|
||||||
|
|
||||||
export function Form() {
|
export function Form() {
|
||||||
const svgContainerRef = useRef<HTMLDivElement>(null);
|
const svgContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const accordionRef1 = useRef<AccordionRef>(null);
|
||||||
|
const accordionRef0 = useRef<AccordionRef>(null);
|
||||||
const { sceneView } = useContext(SceneViewContext) as SceneViewContextType;
|
const { sceneView } = useContext(SceneViewContext) as SceneViewContextType;
|
||||||
|
|
||||||
function handleChange() {
|
function handleChange() {
|
||||||
|
@ -133,11 +168,10 @@ export function Form() {
|
||||||
if (!sceneView) return;
|
if (!sceneView) return;
|
||||||
|
|
||||||
if ((e.target as HTMLInputElement).checked) {
|
if ((e.target as HTMLInputElement).checked) {
|
||||||
sceneView.enableRaycaster();
|
// Enable raycaster with callback to handle svg element
|
||||||
sceneView.addEventListener("svg-created", handleSVGCreated);
|
sceneView.enableRaycaster(handleSVGCreated);
|
||||||
} else {
|
} else {
|
||||||
sceneView.disableRaycaster();
|
sceneView.disableRaycaster();
|
||||||
sceneView.removeEventListener("svg-created", handleSVGCreated);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,13 +183,20 @@ export function Form() {
|
||||||
svgContainerRef.current.removeChild(c);
|
svgContainerRef.current.removeChild(c);
|
||||||
}
|
}
|
||||||
svgContainerRef.current.appendChild(e.detail.element);
|
svgContainerRef.current.appendChild(e.detail.element);
|
||||||
|
if (accordionRef0.current) {
|
||||||
|
accordionRef0.current.open(false);
|
||||||
|
}
|
||||||
|
if (accordionRef1.current) {
|
||||||
|
accordionRef1.current.open(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col gap-2 overflow-y-auto">
|
<div className="w-full max-h-full flex flex-col gap-2">
|
||||||
<div className="w-full flex flex-col gap-3 p-4 border border-gray-200 rounded shadow">
|
<div className="w-full h-full flex flex-col gap-3 p-4 border border-gray-200 rounded shadow">
|
||||||
|
<div className="border border-gray-200 rounded grid grid-cols-2 gap-y-2 p-2">
|
||||||
<Toggle title="Slicing Box" onChange={handleChange} />
|
<Toggle title="Slicing Box" onChange={handleChange} />
|
||||||
<Toggle title="Drilling Profiler" onChange={handleDrilling} />
|
<Toggle title="Virtual Profile" onChange={handleDrilling} />
|
||||||
<Toggle title="Coordinate Grid" onChange={handleChangeCG} />
|
<Toggle title="Coordinate Grid" onChange={handleChangeCG} />
|
||||||
<Toggle title="Wireframe" onChange={handleChangeWireframe} />
|
<Toggle title="Wireframe" onChange={handleChangeWireframe} />
|
||||||
<Toggle
|
<Toggle
|
||||||
|
@ -163,7 +204,14 @@ export function Form() {
|
||||||
onChange={handleChangeTopography}
|
onChange={handleChangeTopography}
|
||||||
defaultChecked
|
defaultChecked
|
||||||
/>
|
/>
|
||||||
<Accordion title="Layers">
|
</div>
|
||||||
|
<div className="overflow-y-auto">
|
||||||
|
<Accordion
|
||||||
|
title="Layers"
|
||||||
|
position={Position.Start}
|
||||||
|
open={true}
|
||||||
|
ref={accordionRef0}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{sceneView?.model.children.map((child) => {
|
{sceneView?.model.children.map((child) => {
|
||||||
|
@ -203,7 +251,15 @@ export function Form() {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
<Accordion
|
||||||
|
title="Virtual Profile"
|
||||||
|
position={Position.Center}
|
||||||
|
open={false}
|
||||||
|
ref={accordionRef1}
|
||||||
|
>
|
||||||
<div ref={svgContainerRef}> </div>
|
<div ref={svgContainerRef}> </div>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { buildCoordinateGrid } from "./utils/build-coordinate-grid";
|
||||||
import { DragControls } from "three/examples/jsm/Addons.js";
|
import { DragControls } from "three/examples/jsm/Addons.js";
|
||||||
import { MapTilerProvider, MapView, OpenStreetMapsProvider } from "geo-three";
|
import { MapTilerProvider, MapView, OpenStreetMapsProvider } from "geo-three";
|
||||||
import { CustomMapHeightNodeShader } from "./CustomMapHeightNodeShader";
|
import { CustomMapHeightNodeShader } from "./CustomMapHeightNodeShader";
|
||||||
import { createSVG } from "./utils/create-borehole-svg";
|
import { Data, createSVG } from "./utils/create-borehole-svg";
|
||||||
|
|
||||||
export type CustomEvent = CustomEventInit<{
|
export type CustomEvent = CustomEventInit<{
|
||||||
element: SVGSVGElement | null;
|
element: SVGSVGElement | null;
|
||||||
|
@ -41,6 +41,7 @@ export class SceneView extends EventTarget {
|
||||||
private _startY: number = 0;
|
private _startY: number = 0;
|
||||||
private _isDragging: boolean = false;
|
private _isDragging: boolean = false;
|
||||||
private static _DRAG_THRESHOLD = 5;
|
private static _DRAG_THRESHOLD = 5;
|
||||||
|
private _callback: EventListenerOrEventListenerObject | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
|
@ -183,21 +184,36 @@ export class SceneView extends EventTarget {
|
||||||
const meshes = this._model.children.filter((c) => c.name !== "Topography");
|
const meshes = this._model.children.filter((c) => c.name !== "Topography");
|
||||||
const intersects = this._raycaster.intersectObjects(meshes, true);
|
const intersects = this._raycaster.intersectObjects(meshes, true);
|
||||||
|
|
||||||
|
// Remove existing point and add visual marker
|
||||||
|
this._removePoint();
|
||||||
this._addPoint(targetPosition);
|
this._addPoint(targetPosition);
|
||||||
|
|
||||||
|
// Iterate over intersections
|
||||||
if (intersects.length > 0) {
|
if (intersects.length > 0) {
|
||||||
const data = [];
|
const data: Data[] = [];
|
||||||
for (let i = 0; i < intersects.length; i += 2) {
|
for (let i = 0; i < intersects.length; i += 2) {
|
||||||
const depthStart = intersects[i].point.z;
|
const depthStart = intersects[i].point.z;
|
||||||
const depthEnd = intersects[i + 1].point.z;
|
const depthEnd = intersects[i + 1].point.z;
|
||||||
let name = intersects[i].object.name;
|
const name = intersects[i].object.name;
|
||||||
let color = `#${(
|
const color = `#${(
|
||||||
(intersects[i].object as Mesh).material as MeshStandardMaterial
|
(intersects[i].object as Mesh).material as MeshStandardMaterial
|
||||||
).color.getHexString()}`;
|
).color.getHexString()}`;
|
||||||
|
|
||||||
|
// Avoid duplicate entries, just update the depth information
|
||||||
|
const index = data.findIndex((d) => d.name === name);
|
||||||
|
if (index > -1) {
|
||||||
|
data[index] = {
|
||||||
|
depthStart: data[index].depthStart,
|
||||||
|
depthEnd,
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
data.push({ depthStart, depthEnd, name, color });
|
data.push({ depthStart, depthEnd, name, color });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const element = createSVG(data, 400, 800, this._extent);
|
const element = createSVG(data, 400, 600, this._extent);
|
||||||
const event = new CustomEvent("svg-created", {
|
const event = new CustomEvent("svg-created", {
|
||||||
detail: { element },
|
detail: { element },
|
||||||
});
|
});
|
||||||
|
@ -226,10 +242,14 @@ export class SceneView extends EventTarget {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
enableRaycaster() {
|
enableRaycaster(callback: EventListenerOrEventListenerObject) {
|
||||||
this._container.addEventListener("pointerdown", this._pointerDownListener);
|
this._container.addEventListener("pointerdown", this._pointerDownListener);
|
||||||
this._container.addEventListener("pointermove", this._pointerMoveListener);
|
this._container.addEventListener("pointermove", this._pointerMoveListener);
|
||||||
this._container.addEventListener("pointerup", this._pointerUpListener);
|
this._container.addEventListener("pointerup", this._pointerUpListener);
|
||||||
|
|
||||||
|
// Add event listener for svg-created event
|
||||||
|
this.addEventListener("svg-created", callback);
|
||||||
|
this._callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
disableRaycaster() {
|
disableRaycaster() {
|
||||||
|
@ -242,16 +262,30 @@ export class SceneView extends EventTarget {
|
||||||
this._pointerMoveListener
|
this._pointerMoveListener
|
||||||
);
|
);
|
||||||
this._container.removeEventListener("pointerup", this._pointerUpListener);
|
this._container.removeEventListener("pointerup", this._pointerUpListener);
|
||||||
|
|
||||||
|
if (this._callback) {
|
||||||
|
this.removeEventListener("svg-created", this._callback);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add point marker for bore profiles
|
||||||
private _addPoint(point: Vector3) {
|
private _addPoint(point: Vector3) {
|
||||||
const geometry = new SphereGeometry(2500, 16, 16); // Small sphere
|
const geometry = new SphereGeometry(1000, 16, 16);
|
||||||
const material = new MeshBasicMaterial({ color: 0xff0000 }); // Red color
|
const material = new MeshBasicMaterial({ color: 0xff0000 });
|
||||||
const sphere = new Mesh(geometry, material);
|
const sphere = new Mesh(geometry, material);
|
||||||
|
sphere.name = "point-marker";
|
||||||
|
|
||||||
sphere.position.set(point.x, point.y, point.z);
|
sphere.position.set(point.x, point.y, point.z);
|
||||||
this._scene.add(sphere);
|
this._scene.add(sphere);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _removePoint() {
|
||||||
|
const o = this._scene.getObjectByName("point-marker");
|
||||||
|
|
||||||
|
if (o) {
|
||||||
|
this._scene.remove(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init(container: HTMLElement, modelId = MODEL_ID) {
|
async function init(container: HTMLElement, modelId = MODEL_ID) {
|
||||||
|
|
|
@ -2,9 +2,10 @@ import * as d3 from "d3";
|
||||||
import { Extent } from "./build-scene";
|
import { Extent } from "./build-scene";
|
||||||
|
|
||||||
// SVG dimensions
|
// SVG dimensions
|
||||||
const margin = { top: 20, right: 250, bottom: 20, left: 20 };
|
const margin = { top: 20, right: 20, bottom: 20, left: 80 };
|
||||||
|
const barWidth = 30;
|
||||||
|
|
||||||
interface Data {
|
export interface Data {
|
||||||
depthStart: number;
|
depthStart: number;
|
||||||
depthEnd: number;
|
depthEnd: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -17,7 +18,6 @@ export function createSVG(
|
||||||
height: number = 800,
|
height: number = 800,
|
||||||
extent: Extent
|
extent: Extent
|
||||||
) {
|
) {
|
||||||
console.log(data);
|
|
||||||
const svg = d3
|
const svg = d3
|
||||||
.create("svg")
|
.create("svg")
|
||||||
.attr("width", width)
|
.attr("width", width)
|
||||||
|
@ -27,34 +27,110 @@ export function createSVG(
|
||||||
|
|
||||||
// Scales: Invert Y-axis so depth increases downward
|
// Scales: Invert Y-axis so depth increases downward
|
||||||
const zmax = d3.max(data, (d) => d.depthStart) ?? extent.zmax;
|
const zmax = d3.max(data, (d) => d.depthStart) ?? extent.zmax;
|
||||||
const zmin = d3.max(data, (d) => d.depthEnd) ?? extent.zmin;
|
const zmin = d3.min(data, (d) => d.depthEnd) ?? extent.zmin;
|
||||||
const zScale = d3
|
const zScale = d3
|
||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
.domain([zmax, zmin])
|
.domain([zmax, zmin])
|
||||||
.range([margin.top, height - margin.bottom]);
|
.range([margin.top, height - margin.bottom]);
|
||||||
|
|
||||||
|
// Create logical group
|
||||||
|
const barGroup = svg.append("g");
|
||||||
|
|
||||||
// Draw bars (formations)
|
// Draw bars (formations)
|
||||||
svg
|
barGroup
|
||||||
.append("g")
|
.selectAll("rect")
|
||||||
.selectAll()
|
|
||||||
.data(data)
|
.data(data)
|
||||||
.join("rect")
|
.join("rect")
|
||||||
.attr("x", margin.left)
|
.attr("x", margin.left)
|
||||||
.attr("y", (d) => zScale(d.depthStart))
|
.attr("y", (d) => zScale(d.depthStart))
|
||||||
.attr("height", (d) => zScale(d.depthEnd) - zScale(d.depthStart))
|
.attr("height", (d) => zScale(d.depthEnd) - zScale(d.depthStart))
|
||||||
.attr("width", width - margin.left - margin.right)
|
.attr("width", barWidth)
|
||||||
.attr("fill", (d) => d.color);
|
.attr("fill", (d) => d.color);
|
||||||
|
|
||||||
// Add labels (formation names)
|
// Add labels (formation names)
|
||||||
svg
|
barGroup
|
||||||
.selectAll(".label")
|
.selectAll("text")
|
||||||
.data(data)
|
.data(data)
|
||||||
.enter()
|
.join("text")
|
||||||
.append("text")
|
.attr("x", margin.left + barWidth + 5)
|
||||||
.attr("class", "label")
|
.attr("y", (d) => (zScale(d.depthStart) + zScale(d.depthEnd)) / 2)
|
||||||
.attr("x", width - margin.right + 5) // Place text slightly outside the bar
|
.attr("text-anchor", "start")
|
||||||
.attr("y", (d) => (zScale(d.depthStart) + zScale(d.depthEnd)) / 2) // Center in the bar
|
.attr("fill", "black")
|
||||||
.text((d) => d.name);
|
.style("font-size", "12px")
|
||||||
|
.each(function (d) {
|
||||||
|
const textElement = d3.select(this);
|
||||||
|
textElement.selectAll("tspan").remove(); // Clear previous tspans
|
||||||
|
|
||||||
|
const groups = groupWordsByFour(d.name);
|
||||||
|
let dy = 0;
|
||||||
|
|
||||||
|
for (const group of groups) {
|
||||||
|
textElement
|
||||||
|
.append("tspan")
|
||||||
|
.attr("x", margin.left + barWidth + 5)
|
||||||
|
.attr("dy", dy)
|
||||||
|
.text(group.join(" "));
|
||||||
|
|
||||||
|
dy = 14;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add depth labels
|
||||||
|
svg
|
||||||
|
.append("g")
|
||||||
|
.selectAll("text")
|
||||||
|
.data(data)
|
||||||
|
.join("text")
|
||||||
|
.attr("x", margin.left - 5)
|
||||||
|
.attr("y", (d) =>
|
||||||
|
d.depthStart - d.depthEnd < 100
|
||||||
|
? zScale(d.depthStart) - 10
|
||||||
|
: zScale(d.depthStart)
|
||||||
|
)
|
||||||
|
.attr("dy", "0.35em")
|
||||||
|
.attr("text-anchor", "end")
|
||||||
|
.attr("fill", "black")
|
||||||
|
.style("font-size", "12px")
|
||||||
|
.text((d) => `${d.depthStart.toFixed(0)}m`);
|
||||||
|
|
||||||
|
// Add label for last depth
|
||||||
|
svg
|
||||||
|
.append("g")
|
||||||
|
.selectAll("text")
|
||||||
|
.data(data)
|
||||||
|
.join("text")
|
||||||
|
.attr("x", margin.left - 5)
|
||||||
|
.attr("y", (d, i) => (i === data.length - 1 ? zScale(d.depthEnd) : null))
|
||||||
|
.attr("dy", "0.35em")
|
||||||
|
.attr("text-anchor", "end")
|
||||||
|
.attr("fill", "black")
|
||||||
|
.style("font-size", "12px")
|
||||||
|
.text((d, i) => (i === data.length - 1 ? `${d.depthEnd.toFixed(0)}m` : ""));
|
||||||
|
|
||||||
return svg.node();
|
return svg.node();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Group words to split lines if necessary
|
||||||
|
function groupWordsByFour(inputString: string) {
|
||||||
|
const words = inputString.split(" ");
|
||||||
|
|
||||||
|
// Use reduce to group the words into chunks of four
|
||||||
|
const groups = words.reduce(
|
||||||
|
(result: string[][], word: string, index: number) => {
|
||||||
|
const groupIndex = Math.floor(index / 4);
|
||||||
|
|
||||||
|
// If the group doesn't exist yet, create an empty array for it
|
||||||
|
if (!result[groupIndex]) {
|
||||||
|
result[groupIndex] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the current word to the correct group
|
||||||
|
result[groupIndex].push(word);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue