AIdentity

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit a76446155c8b43528a7c03e9f0c16be557c9f8a4
parent 321880ae975eac13b69e5fd06d74419399653ebd
Author: minerva-jupiter <ryouturn@gmail.com>
Date:   Fri, 24 Oct 2025 00:00:23 +0900

feat(draw): Implement WASM-based SVG path snapping and dynamic loading

Diffstat:
Mapp/components/2_draw.tsx | 259+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mapp/ctrl/page.tsx | 3+++
Mpublic/whiteperson.svg | 26++++++++------------------
Mrust-wasm/Cargo.lock | 33+++++++++++++++++++++++++++++++++
Mrust-wasm/Cargo.toml | 1+
Mrust-wasm/pkg/rust_wasm.d.ts | 21+++++++++++++++++++++
Mrust-wasm/pkg/rust_wasm.js | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrust-wasm/pkg/rust_wasm_bg.wasm | 0
Mrust-wasm/pkg/rust_wasm_bg.wasm.d.ts | 7+++++++
Mrust-wasm/src/lib.rs | 407+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
10 files changed, 655 insertions(+), 177 deletions(-)

diff --git a/app/components/2_draw.tsx b/app/components/2_draw.tsx @@ -1,36 +1,23 @@ 'use client' -import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react'; -// ⚠️ WASMモジュールのインポート。パスはプロジェクト構造に合わせてください。 -// 例: import { find_nearest_point_on_path } from '@/wasm/pkg/snap_calculator'; - -// WASMモジュールが返すべき型を定義 -interface NearestPointResult { - x: number; - y: number; -} -// find_nearest_point_on_path関数のダミー宣言 (WASMの実際のインポートに置き換え) -declare function find_nearest_point_on_path( - path_d: string, - target_x: number, - target_y: number, - max_distance: number -): NearestPointResult | null; - -// --- 型定義 --- -interface StageProps { onComplete: () => void; } -type Point = { x: number; y: number; }; +import React, { useState, useRef, useCallback, useEffect } from 'react'; +import init, { find_nearest_point_on_path, NearestPointResult } from '../../rust-wasm/pkg/rust_wasm.js'; +import { StageProps } from '../ctrl/page.tsx'; + +type Tool = 'pen'; +type Point = { x: number; y: number; }; interface LineData { id: number; - tool: 'pen'; + tool: Tool; points: Point[]; targetPoints: Point[]; } -// ⚠️ 実際のSVGのパスデータ。このパスデータに基づいてWASMが計算します。 -const BACKGROUND_SVG_PATH_D = "M100 100 L400 100 L400 400 L100 400 Z"; -const SNAPPING_DISTANCE_PIXELS = 50; // 吸着距離 (50px) +const BACKGROUND_SVG_PATH_D_DEFAULT = ""; +const VIEWSBOX_SIZE = 500; +const SNAPPING_DISTANCE_PIXELS = 30; + function DrawingApp({onComplete}: StageProps) { const [isClient, setIsClient] = useState(false); @@ -38,6 +25,11 @@ function DrawingApp({onComplete}: StageProps) { const [lines, setLines] = useState<LineData[]>([]); const [currentLines, setCurrentLines] = useState<LineData[]>([]); + const [stageWidth, setStageWidth] = useState(0); + const [stageHeight, setStageHeight] = useState(0); + const [backgroundPathD, setBackgroundPathD] = useState(BACKGROUND_SVG_PATH_D_DEFAULT); + const [viewBoxSize, setViewBoxSize] = useState(VIEWSBOX_SIZE); + const isDrawing = useRef(false); const animationRef = useRef<number>(0); @@ -45,54 +37,62 @@ function DrawingApp({onComplete}: StageProps) { const stageRef = useRef<HTMLDivElement>(null); - // Stageサイズはビューボックスの標準値として500を基準に、画面サイズに合わせてスケーリングします。 - const VIEWSBOX_SIZE = 500; - const [stageSize, setStageSize] = useState(0); + useEffect(() => { + init(); + },[]) + // --- 1. サイズ計算とステージ設定 (画面全体を使用) --- useEffect(() => { setIsClient(true); const handleResize = () => { - if (typeof window !== 'undefined') { - const size = Math.min(window.innerWidth, window.innerHeight); - // 画面の小さい方に合わせて90%のサイズを設定 - setStageSize(size * 0.9); + if (typeof globalThis !== 'undefined') { + setStageWidth(globalThis.innerWidth); + setStageHeight(globalThis.innerHeight); } }; handleResize(); - window.addEventListener('resize', handleResize); + globalThis.addEventListener('resize', handleResize); return () => { - window.removeEventListener('resize', handleResize); + globalThis.removeEventListener('resize', handleResize); }; }, []); + + // --- 2. 座標変換ユーティリティ --- + + const scaleToViewBox = useCallback((p: Point): Point => { + if (stageWidth === 0 || stageHeight === 0) return p; + + const effectiveSize = Math.min(stageWidth, stageHeight); + const scale = viewBoxSize / effectiveSize; + const offsetX = (stageWidth - effectiveSize) / 2; + const offsetY = (stageHeight - effectiveSize) / 2; - // --- 座標変換ユーティリティ --- - // 画面座標をSVGビューボックス座標にスケーリング - const scaleToViewBox = useCallback((p: Point): Point => { - if (stageSize === 0) return p; - const scale = VIEWSBOX_SIZE / stageSize; return { - x: p.x * scale, - y: p.y * scale, + x: (p.x - offsetX) * scale, + y: (p.y - offsetY) * scale, }; - }, [stageSize]); + }, [stageWidth, stageHeight, viewBoxSize]); - // SVGビューボックス座標を画面座標にスケーリング const scaleToScreen = useCallback((p: Point): Point => { - if (stageSize === 0) return p; - const scale = stageSize / VIEWSBOX_SIZE; + if (stageWidth === 0 || stageHeight === 0) return p; + + const effectiveSize = Math.min(stageWidth, stageHeight); + const scale = effectiveSize / viewBoxSize; + + const offsetX = (stageWidth - effectiveSize) / 2; + const offsetY = (stageHeight - effectiveSize) / 2; + return { - x: p.x * scale, - y: p.y * scale, + x: p.x * scale + offsetX, + y: p.y * scale + offsetY, }; - }, [stageSize]); - + }, [stageWidth, stageHeight, viewBoxSize]); - // ユーティリティ関数: マウス/タッチ座標の取得 (画面座標) - const getPointerPosition = useCallback((e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent): Point | null => { + const getPointerPosition = useCallback((e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent): Point | null => { if (!stageRef.current) return null; const rect = stageRef.current.getBoundingClientRect(); @@ -108,51 +108,106 @@ function DrawingApp({onComplete}: StageProps) { return { x: clientX - rect.left, y: clientY - rect.top }; }, []); + + // --- 3. SVGコンテンツの取得ロジック (DOMParser) --- + useEffect(() => { + const fetchAndParseSvg = async () => { + try { + const response = await fetch('/whiteperson.svg'); + const svgText = await response.text(); + + const parser = new DOMParser(); + const doc = parser.parseFromString(svgText, "image/svg+xml"); + + const svgElement = doc.querySelector('svg'); + if (svgElement) { + const viewBoxAttr = svgElement.getAttribute('viewBox'); + if (viewBoxAttr) { + const parts = viewBoxAttr.trim().split(/\s+/); + if (parts.length === 4) { + const size = Math.max(parseFloat(parts[2]), parseFloat(parts[3])); + if (!isNaN(size) && size > 0) { + setViewBoxSize(size); + } + } + } + } + + const pathElements = doc.querySelectorAll('path'); + let foundDValue = null; + + for (const element of Array.from(pathElements)) { + const dValue = element.getAttribute('d'); + if (dValue && dValue.trim().length > 0) { + foundDValue = dValue; + break; + } + } + + if (foundDValue) { + setBackgroundPathD(foundDValue); + return; + } + + console.error("Error: <path> element or 'd' attribute not found in whiteperson.svg."); + + } catch (error) { + console.error("Failed to load or parse whiteperson.svg:", error); + } + }; + + if (isClient && backgroundPathD === "") { + fetchAndParseSvg(); + } + }, [isClient, backgroundPathD]); - // --- 吸着ロジック (WASM利用) --- + // --- 4. 吸着ロジック (WASM呼び出し) --- const calculateNearestTargetPoint = useCallback((p: Point): Point => { - // 描画点 (画面座標) を WASM に渡す前にビューボックス座標に変換 const p_viewbox = scaleToViewBox(p); - // ⚠️ WASMの代わりにダミー関数を使用 - /* - const nearest_viewbox = find_nearest_point_on_path( - BACKGROUND_SVG_PATH_D, + const effectiveSize = Math.min(stageWidth, stageHeight); + const snapping_distance_viewbox = SNAPPING_DISTANCE_PIXELS * (viewBoxSize / effectiveSize); + + // ⚠️ WASM呼び出し + const nearest_viewbox: NearestPointResult | null = find_nearest_point_on_path( + backgroundPathD, p_viewbox.x, p_viewbox.y, - SNAPPING_DISTANCE_PIXELS // 距離はビューボックス座標系で計算される + snapping_distance_viewbox ); + console.log("nearest_viewbox is ",nearest_viewbox); if (nearest_viewbox) { - // 見つかったターゲット点 (ビューボックス座標) を画面座標に戻して返す return scaleToScreen(nearest_viewbox); } - */ - - // 吸着しない場合は元の点を返す return p; - - }, [scaleToViewBox, scaleToScreen]); + }, [scaleToViewBox, scaleToScreen, stageWidth, stageHeight, viewBoxSize, backgroundPathD]); const calculateTargetPoints = useCallback((points: Point[]): Point[] => { - // 全点に対してターゲット点を計算 return points.map(p => calculateNearestTargetPoint(p)); }, [calculateNearestTargetPoint]); - // --- イベントハンドラ(前回の実装を継承し、Konvaの型を削除) --- + // --- 5. イベントハンドラ (吹き飛んでいた部分を再定義) --- + + /** + * マウス・タッチ開始時の処理 + */ const handleMouseDown = useCallback((e: React.MouseEvent | React.TouchEvent) => { isDrawing.current = true; const pos = getPointerPosition(e); if (pos) { lineIdCounter.current += 1; - const newLine = { id: lineIdCounter.current, tool: 'pen' as const, points: [pos], targetPoints: [] }; + const newLine: LineData = { id: lineIdCounter.current, tool: 'pen', points: [pos], targetPoints: [] }; setLines(prev => [...prev, newLine]); setCurrentLines(prev => [...prev, newLine]); } }, [getPointerPosition]); + /** + * マウス・タッチ移動時の処理 + */ const handleMouseMove = useCallback((e: React.MouseEvent | React.TouchEvent) => { if (!isDrawing.current) return; const point = getPointerPosition(e); @@ -162,15 +217,18 @@ function DrawingApp({onComplete}: StageProps) { const lastLineIndex = prevLines.length - 1; if (lastLineIndex < 0) return prevLines; const lastLine = prevLines[lastLineIndex]; - const newLine: LineData = { ...lastLine, points: [...lastLine.points, point] }; - return [...prevLines.slice(0, lastLineIndex), newLine]; + const newPoints: Point[] = [...lastLine.points, point]; + return [...prevLines.slice(0, lastLineIndex), { ...lastLine, points: newPoints }]; }; - + setLines(updateLines); setCurrentLines(updateLines); }, [getPointerPosition]); + /** + * マウス・タッチ終了時の処理 + */ const handleMouseUp = useCallback(() => { isDrawing.current = false; @@ -178,8 +236,7 @@ function DrawingApp({onComplete}: StageProps) { const lastLineIndex = prevLines.length - 1; if (lastLineIndex < 0) return prevLines; const lastLine = prevLines[lastLineIndex]; - - // 描画終了時にtargetPointsを計算 + const targetPoints = calculateTargetPoints(lastLine.points); const newLine: LineData = { @@ -190,7 +247,7 @@ function DrawingApp({onComplete}: StageProps) { }); }, [calculateTargetPoints]); - // --- アニメーションロジック(前回の実装を継承) --- + // --- 6. アニメーションロジック (変更なし) --- const animateLines = useCallback((timestamp: number) => { if (!startTimeRef.current) startTimeRef.current = timestamp; @@ -218,7 +275,6 @@ function DrawingApp({onComplete}: StageProps) { animationRef.current = requestAnimationFrame(animateLines); } else { startTimeRef.current = undefined; - // 移動後のラインを lines (マスターデータ)に反映 setLines(newCurrentLines.map(line => ({ ...line, points: line.targetPoints, @@ -241,7 +297,7 @@ function DrawingApp({onComplete}: StageProps) { }; }, [lines, animateLines]); - // points配列を 'x1,y1 x2,y2 ...' 形式の文字列に変換するヘルパー + const pointsToSvgString = (points: Point[]): string => { return points.map(p => `${p.x},${p.y}`).join(' '); }; @@ -250,39 +306,48 @@ function DrawingApp({onComplete}: StageProps) { <div ref={stageRef} style={{ - width: stageSize, - height: stageSize, - margin: 'auto', - position: 'relative', - border: '1px solid #ccc', + width: stageWidth, + height: stageHeight, + margin: 0, + position: 'fixed', + top: 0, + left: 0, overflow: 'hidden', touchAction: 'none', }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} - onTouchStart={handleMouseDown as any} - onTouchMove={handleMouseMove as any} + onTouchStart={handleMouseDown} + onTouchMove={handleMouseMove} onTouchEnd={handleMouseUp} > - {/* 1. 背景のSVG (模倣対象) - ビューボックスを使用 */} - <svg width={stageSize} height={stageSize} viewBox={`0 0 ${VIEWSBOX_SIZE} ${VIEWSBOX_SIZE}`} style={{ position: 'absolute' }}> - {/* BACKGROUND_SVG_PATH_Dは VIEWSBOX_SIZE の座標系で定義されていることを前提とする */} - <path - d={BACKGROUND_SVG_PATH_D} - fill="none" - stroke="black" - strokeWidth="5" - opacity="0.2" - /> - </svg> - - {/* 2. ユーザーの描画 (アニメーション表示用) - ビューボックスを使用 */} - <svg width={stageSize} height={stageSize} viewBox={`0 0 ${VIEWSBOX_SIZE} ${VIEWSBOX_SIZE}`} style={{ position: 'absolute', top: 0, left: 0 }}> + {/* 1. 背景のSVG (模倣対象) */} + {backgroundPathD && ( + <svg + width={stageWidth} + height={stageHeight} + viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`} + style={{ position: 'absolute' }} + preserveAspectRatio="xMidYMid meet" + > + <path + d={backgroundPathD} + fill="none" + stroke="white" + strokeWidth="5" + opacity="0.2" + /> + </svg> + )} + + {/* 2. ユーザーの描画 (アニメーション表示用) */} + <svg width={stageWidth} height={stageHeight} viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`} style={{ position: 'absolute', top: 0, left: 0 }} + preserveAspectRatio="xMidYMid meet" + > {currentLines.map((line) => ( <polyline key={line.id} - // 描画はビューボックス座標系で行われる points={pointsToSvgString(line.points.map(scaleToViewBox))} fill="none" stroke="#FF4500" @@ -296,7 +361,7 @@ function DrawingApp({onComplete}: StageProps) { <button type="submit" onClick={onComplete} - style={{ position: 'absolute', bottom: 10, left: 10 }} + style={{ position: 'fixed', bottom: 10, left: 10 }} > 完了 (onComplete) </button> diff --git a/app/ctrl/page.tsx b/app/ctrl/page.tsx @@ -31,6 +31,7 @@ export default function Chat() { }, []); let StageComponent: React.ReactNode; + /* switch (stage) { case 1: StageComponent = <FirstChat onComplete={handleStageComplete}/>; @@ -41,6 +42,8 @@ export default function Chat() { default: StageComponent = <Error onComplete={handleStageComplete}/>; } + */ + StageComponent = <SecondDraw onComplete={handleStageComplete}/>; const checkFullscreen = () => { const isTargetFullscreen = document.fullscreenElement === pageRef.current; setIsFullscreen(isTargetFullscreen); diff --git a/public/whiteperson.svg b/public/whiteperson.svg @@ -9,22 +9,12 @@ id="svg1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> - <defs - id="defs1" /> - <g - id="layer1"> - <circle - style="fill:#ffffff;stroke-width:0.837442" - id="path1" - cx="257.87497" - cy="195.09868" - r="150" /> - <ellipse - style="fill:#ffffff;stroke-width:1.18432" - id="path1-8" - cx="251.02637" - cy="453.14139" - rx="300" - ry="150" /> - </g> + <path + stroke="white" + stroke-width="5" + fill="none" + d="M 100 250 +A 150 150 0 1 0 400 250 +A 150 150 0 1 0 100 250" + /> </svg> diff --git a/rust-wasm/Cargo.lock b/rust-wasm/Cargo.lock @@ -88,6 +88,12 @@ dependencies = [ ] [[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -211,6 +217,15 @@ dependencies = [ ] [[package]] +name = "euclid" +version = "0.22.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +dependencies = [ + "num-traits", +] + +[[package]] name = "find-msvc-tools" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -346,6 +361,17 @@ dependencies = [ ] [[package]] +name = "kurbo" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9729cc38c18d86123ab736fd2e7151763ba226ac2490ec092d1dd148825e32" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -542,6 +568,7 @@ dependencies = [ "getrandom 0.2.16", "getrandom 0.3.4", "js-sys", + "kurbo", "vibrato", "wasm-bindgen", "zstd", @@ -627,6 +654,12 @@ dependencies = [ ] [[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] name = "syn" version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/rust-wasm/Cargo.toml b/rust-wasm/Cargo.toml @@ -13,3 +13,4 @@ wasm-bindgen = "0.2.104" getrandom_v02 = { package = "getrandom", version = "0.2", features = ["js"] } getrandom_v03 = { package = "getrandom", version = "0.3", features = ["wasm_js"] } zstd = "0.13.3" +kurbo = "0.12.0" diff --git a/rust-wasm/pkg/rust_wasm.d.ts b/rust-wasm/pkg/rust_wasm.d.ts @@ -1,12 +1,33 @@ /* tslint:disable */ /* eslint-disable */ export function chat(dict_data: Uint8Array, input: string): string; +/** + * + * * SVGパス上で指定された点に最も近い点を計算します。 + * + */ +export function find_nearest_point_on_path(path_d: string, x: number, y: number, _snapping_distance_viewbox: number): NearestPointResult | undefined; +export function main_js(): void; +export class NearestPointResult { + private constructor(); + free(): void; + [Symbol.dispose](): void; + x: number; + y: number; +} export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export interface InitOutput { readonly memory: WebAssembly.Memory; readonly chat: (a: number, b: number, c: number, d: number) => [number, number, number, number]; + readonly __wbg_nearestpointresult_free: (a: number, b: number) => void; + readonly __wbg_get_nearestpointresult_x: (a: number) => number; + readonly __wbg_set_nearestpointresult_x: (a: number, b: number) => void; + readonly __wbg_get_nearestpointresult_y: (a: number) => number; + readonly __wbg_set_nearestpointresult_y: (a: number, b: number) => void; + readonly find_nearest_point_on_path: (a: number, b: number, c: number, d: number, e: number) => number; + readonly main_js: () => void; readonly rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void; readonly rust_zstd_wasm_shim_malloc: (a: number) => number; readonly rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number; diff --git a/rust-wasm/pkg/rust_wasm.js b/rust-wasm/pkg/rust_wasm.js @@ -124,6 +124,81 @@ export function chat(dict_data, input) { } } +/** + * + * * SVGパス上で指定された点に最も近い点を計算します。 + * + * @param {string} path_d + * @param {number} x + * @param {number} y + * @param {number} _snapping_distance_viewbox + * @returns {NearestPointResult | undefined} + */ +export function find_nearest_point_on_path(path_d, x, y, _snapping_distance_viewbox) { + const ptr0 = passStringToWasm0(path_d, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.find_nearest_point_on_path(ptr0, len0, x, y, _snapping_distance_viewbox); + return ret === 0 ? undefined : NearestPointResult.__wrap(ret); +} + +export function main_js() { + wasm.main_js(); +} + +const NearestPointResultFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_nearestpointresult_free(ptr >>> 0, 1)); + +export class NearestPointResult { + + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(NearestPointResult.prototype); + obj.__wbg_ptr = ptr; + NearestPointResultFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + NearestPointResultFinalization.unregister(this); + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_nearestpointresult_free(ptr, 0); + } + /** + * @returns {number} + */ + get x() { + const ret = wasm.__wbg_get_nearestpointresult_x(this.__wbg_ptr); + return ret; + } + /** + * @param {number} arg0 + */ + set x(arg0) { + wasm.__wbg_set_nearestpointresult_x(this.__wbg_ptr, arg0); + } + /** + * @returns {number} + */ + get y() { + const ret = wasm.__wbg_get_nearestpointresult_y(this.__wbg_ptr); + return ret; + } + /** + * @param {number} arg0 + */ + set y(arg0) { + wasm.__wbg_set_nearestpointresult_y(this.__wbg_ptr, arg0); + } +} +if (Symbol.dispose) NearestPointResult.prototype[Symbol.dispose] = NearestPointResult.prototype.free; + const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']); async function __wbg_load(module, imports) { diff --git a/rust-wasm/pkg/rust_wasm_bg.wasm b/rust-wasm/pkg/rust_wasm_bg.wasm Binary files differ. diff --git a/rust-wasm/pkg/rust_wasm_bg.wasm.d.ts b/rust-wasm/pkg/rust_wasm_bg.wasm.d.ts @@ -2,6 +2,13 @@ /* eslint-disable */ export const memory: WebAssembly.Memory; export const chat: (a: number, b: number, c: number, d: number) => [number, number, number, number]; +export const __wbg_nearestpointresult_free: (a: number, b: number) => void; +export const __wbg_get_nearestpointresult_x: (a: number) => number; +export const __wbg_set_nearestpointresult_x: (a: number, b: number) => void; +export const __wbg_get_nearestpointresult_y: (a: number) => number; +export const __wbg_set_nearestpointresult_y: (a: number, b: number) => void; +export const find_nearest_point_on_path: (a: number, b: number, c: number, d: number, e: number) => number; +export const main_js: () => void; export const rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void; export const rust_zstd_wasm_shim_malloc: (a: number) => number; export const rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number; diff --git a/rust-wasm/src/lib.rs b/rust-wasm/src/lib.rs @@ -1,4 +1,4 @@ -use std::{io:: Cursor, usize}; +use std::{io:: Cursor, iter::Peekable, str::SplitWhitespace, usize}; use vibrato::{Dictionary, Tokenizer}; use wasm_bindgen::prelude::*; @@ -32,81 +32,364 @@ pub fn chat(dict_data: &[u8], input: &str) -> Result<String, JsValue> { Ok(ans) } +use kurbo::{Point, BezPath, ParamCurve, PathSeg, Vec2}; +use kurbo::ParamCurveNearest; +use wasm_bindgen::JsValue; +use std::f64::consts::PI; + +// TypeScriptのPoint型に対応する構造体を定義 +#[derive(Debug, Clone, Copy)] #[wasm_bindgen] pub struct NearestPointResult { - x: f64, - y: f64, + pub x: f64, + pub y: f64, } -#[wasm_bindgen] -impl NearestPointResult { - pub fn new(x: f64, y: f64) -> NearestPointResult { - NearestPointResult { x, y } +// ------------------------------------------------------------------------- +// 🛠️ Arc to Bezier 変換ヘルパ +// ------------------------------------------------------------------------- + +/// ベクトルVを回転角度φだけ回転させます。(名前を rotate_vec2 に変更) +fn rotate_vec2(v: Vec2, sin_phi: f64, cos_phi: f64) -> Vec2 { + Vec2::new( + v.x * cos_phi - v.y * sin_phi, + v.x * sin_phi + v.y * cos_phi, + ) +} + +/// 単一のArcセグメント(2つの角度間)をCubic Bezierに変換します。 +fn segment_to_cubic( + center: Point, + radii: Point, // (rx, ry) + phi: f64, // x軸回転角度 + start_angle: f64, + delta_angle: f64, + path: &mut BezPath, + current_point: Point, +) -> Point { + + if delta_angle.abs() < 1e-6 { + return current_point; } - #[wasm_bindgen(getter)] - pub fn x(&self) -> f64 { self.x } - #[wasm_bindgen(getter)] - pub fn y(&self) -> f64 { self.y } + let t_factor = 4.0 / 3.0 * (delta_angle / 4.0).tan(); + + let a = radii.x; + let b = radii.y; + let cos_phi = phi.cos(); + let sin_phi = phi.sin(); + + // Vec2として扱う + let start_vec = Vec2::new(start_angle.cos() * a, start_angle.sin() * b); + let end_vec = Vec2::new((start_angle + delta_angle).cos() * a, (start_angle + delta_angle).sin() * b); + + // Vec2を回転 + let start_vec_rotated = rotate_vec2(start_vec, sin_phi, cos_phi); + let end_vec_rotated = rotate_vec2(end_vec, sin_phi, cos_phi); + + let p0 = center + start_vec_rotated; + let p3 = center + end_vec_rotated; + + // Vec2 を使ったタンジェント計算 + let t_start_vec = Vec2::new(-start_vec.y * t_factor, start_vec.x * t_factor); + let t_end_vec = Vec2::new(end_vec.y * t_factor, -end_vec.x * t_factor); + + // Vec2を回転 + let t_start = rotate_vec2(t_start_vec, sin_phi, cos_phi); + let t_end = rotate_vec2(t_end_vec, sin_phi, cos_phi); + + let p1 = p0 + t_start; + let p2 = p3 + t_end; + + path.curve_to(p1, p2, p3); + p3 } + +/// SVG Arcコマンドを複数のCubic Bezierセグメントに分解します。 +fn arc_to_beziers( + start_point: Point, + mut rx: f64, + mut ry: f64, + x_axis_rotation: f64, + large_arc_flag: bool, + sweep_flag: bool, + end_point: Point, + path: &mut BezPath, +) -> Point { + + if (rx.abs() < 1e-6 || ry.abs() < 1e-6) || start_point == end_point { + path.line_to(end_point); + return end_point; + } + + rx = rx.abs(); + ry = ry.abs(); + + let phi = x_axis_rotation * PI / 180.0; + let cos_phi = phi.cos(); + let sin_phi = phi.sin(); + + // Vec2を回転し、結果も Vec2 + let p_vec = (start_point - end_point) * 0.5; + let mut p_prime_vec = rotate_vec2( + p_vec, + -sin_phi, + cos_phi, + ); + // Pointとして使うために一時的に変換 + let p_prime = Point::new(p_prime_vec.x, p_prime_vec.y); + + let lambda = p_prime.x * p_prime.x / (rx * rx) + p_prime.y * p_prime.y / (ry * ry); + if lambda > 1.0 { + let root = lambda.sqrt(); + rx *= root; + ry *= root; + } + + let rx_sq = rx * rx; + let ry_sq = ry * ry; + let x_prime_sq = p_prime.x * p_prime.x; + let y_prime_sq = p_prime.y * p_prime.y; + + let mut center_coeff = ( + (rx_sq * ry_sq - rx_sq * y_prime_sq - ry_sq * x_prime_sq) / + (rx_sq * y_prime_sq + ry_sq * x_prime_sq) + ).max(0.0).sqrt(); + + if large_arc_flag == sweep_flag { + center_coeff = -center_coeff; + } + + let center_prime = Vec2::new( // 中心C'は変位なのでVec2で表現 + center_coeff * rx * p_prime.y / ry, + center_coeff * -ry * p_prime.x / rx, + ); + + // 🚨 修正: 楕円の中心 C を計算 (Vec2で計算し、Point + Vec2 で最終位置を決定) + let center_diff_vec = (start_point - Point::ZERO + end_point.to_vec2()) * 0.5; + + let center_midpoint_vec: Vec2 = (start_point - Point::ZERO + end_point.to_vec2()) * 0.5; + let midpoint: Point = Point::new(center_midpoint_vec.x, center_midpoint_vec.y); + + // C' (Vec2) を回転 + let center_prime_rotated = rotate_vec2(center_prime, sin_phi, cos_phi); + + // 中点 (Point) + 回転変位 (Vec2) = 最終的な中心 (Point) + let center: Point = midpoint + center_prime_rotated; + + // 7. 角度の計算 + let to_angle = |p: Point| -> f64 { + let mut angle = (p.y).atan2(p.x); + if angle < 0.0 { angle += 2.0 * PI; } + angle + }; + + // 始点と終点の角度を計算 (Pointを使って角度を求める) + let start_vec_p = Point::new( + (p_prime.x - center_prime.x) / rx, + (p_prime.y - center_prime.y) / ry, + ); + let mut start_angle = to_angle(start_vec_p); + + let end_vec_p = Point::new( + (-p_prime.x - center_prime.x) / rx, + (-p_prime.y - center_prime.y) / ry, + ); + let mut end_angle = to_angle(end_vec_p); + + // 角度差の計算 + let mut delta_angle = end_angle - start_angle; + if !sweep_flag && delta_angle > 0.0 { + delta_angle -= 2.0 * PI; + } else if sweep_flag && delta_angle < 0.0 { + delta_angle += 2.0 * PI; + } + + let num_segments = (delta_angle.abs() / (PI / 2.0)).ceil() as i32; + let segment_delta = delta_angle / num_segments as f64; + + let mut current_arc_angle = start_angle; + let mut current_p = start_point; + + for _ in 0..num_segments { + let next_angle = current_arc_angle + segment_delta; + + current_p = segment_to_cubic( + center, + Point::new(rx, ry), + phi, + current_arc_angle, + segment_delta, + path, + current_p, + ); + + current_arc_angle = next_angle; + } + + end_point +} + +// ... (残りの関数は変更なし) ... + +/// SVG Path Data (d attribute) を解析し、kurbo::BezPathに変換します。 +fn parse_svg_path_to_bezpath(path_d: &str) -> Option<BezPath> { + let mut path = BezPath::new(); + + let binding = path_d.replace(',', " "); + + let mut tokens: Peekable<SplitWhitespace> = binding.split_whitespace().peekable(); + + let mut current_point = Point::new(0.0, 0.0); + let mut subpath_start = Point::new(0.0, 0.0); + + let get_f64 = | tokens: &mut Peekable<SplitWhitespace>| -> Option<f64> { + tokens.next().and_then(|s| s.parse::<f64>().ok()) + }; + + while let Some(token) = tokens.next() { + let command = token.to_uppercase(); + + match command.as_str() { + "M" => { + let x = get_f64(&mut tokens)?; + let y = get_f64(&mut tokens)?; + current_point = Point::new(x, y); + subpath_start = current_point; + path.move_to(current_point); + + while tokens.peek().and_then(|t| t.parse::<f64>().ok()).is_some() { + let x = get_f64(&mut tokens)?; + let y = get_f64(&mut tokens)?; + current_point = Point::new(x, y); + path.line_to(current_point); + } + } + "L" => { + let x = get_f64(&mut tokens)?; + let y = get_f64(&mut tokens)?; + current_point = Point::new(x, y); + path.line_to(current_point); + } + "C" => { + let x1 = get_f64(&mut tokens)?; + let y1 = get_f64(&mut tokens)?; + let x2 = get_f64(&mut tokens)?; + let y2 = get_f64(&mut tokens)?; + let x = get_f64(&mut tokens)?; + let y = get_f64(&mut tokens)?; + + let p1 = Point::new(x1, y1); + let p2 = Point::new(x2, y2); + current_point = Point::new(x, y); + + path.curve_to(p1, p2, current_point); + } + "A" => { + let rx = get_f64(&mut tokens)?; + let ry = get_f64(&mut tokens)?; + let x_axis_rotation = get_f64(&mut tokens)?; + let large_arc_flag = get_f64(&mut tokens)? != 0.0; + let sweep_flag = get_f64(&mut tokens)? != 0.0; + let x = get_f64(&mut tokens)?; + let y = get_f64(&mut tokens)?; + let end_point = Point::new(x, y); + + // Arc to Bezier 変換ロジックを呼び出し + current_point = arc_to_beziers( + current_point, + rx, + ry, + x_axis_rotation, + large_arc_flag, + sweep_flag, + end_point, + &mut path, + ); + } + "Z" => { + path.close_path(); + current_point = subpath_start; + } + _ => { + return None; + } + } + } + Some(path) +} + + +/** + * SVGパス上で指定された点に最も近い点を計算します。 + */ #[wasm_bindgen] pub fn find_nearest_point_on_path( - path_d: &str, // 背景SVGのパスデータ (d属性) - target_x: f64, // ユーザー描画点のX座標 - target_y: f64, // ユーザー描画点のY座標 - max_snapping_distance: f64 // 吸着距離の閾値 + path_d: &str, + x: f64, + y: f64, + _snapping_distance_viewbox: f64 ) -> Option<NearestPointResult> { - - let target_point = (target_x, target_y); - - // 1. パスのパース: - // path_d をベジェ曲線セグメントのリストに変換 - // let segments = svgtypes::PathParser::from(path_d).collect(); - - let mut min_distance_sq = f64::MAX; - let max_distance_sq = max_snapping_distance * max_snapping_distance; - let mut nearest_point: Option<(f64, f64)> = None; - - // 2. 最短距離計算 (各セグメントに対して実行) - // - 擬似コード: 実際にはベジェ曲線と点の最短距離を計算する幾何学的アルゴリズムが必要。 - // - 非常に複雑なため、ここでは単純にパスを多数の点にサンプリングして探索するアプローチを想定。 - // - // for segment in segments { - // let (closest_pt_on_seg, dist_sq) = segment.find_closest_point(target_point); - // if dist_sq < min_distance_sq { - // min_distance_sq = dist_sq; - // nearest_point = Some(closest_pt_on_seg); - // } - // } - - - // --- 暫定的なシンプルなサンプリングロジックの代替(実際のプロダクトでは置き換えが必要) --- - // ここでは、ダミーの矩形パス (M100 100 L400 100 L400 400 L100 400 Z) をサンプリングした点を仮定します。 - // このダミーは、WASMが正しく動作することを示すためのものです。 - let dummy_edge_points = vec![ - (100.0, 100.0), (150.0, 100.0), (200.0, 100.0), /* ... */ - ]; - - for (ex, ey) in dummy_edge_points { - let dx = ex - target_x; - let dy = ey - target_y; - let dist_sq = dx * dx + dy * dy; - - if dist_sq < min_distance_sq { - min_distance_sq = dist_sq; - nearest_point = Some((ex, ey)); - } - } - // --------------------------------------------------------------------------------- + let target_point = Point::new(x, y); + + let bez_path = parse_svg_path_to_bezpath(path_d)?; - if min_distance_sq < max_distance_sq { - if let Some((x, y)) = nearest_point { - return Some(NearestPointResult::new(x, y)); + let mut closest_point: Option<Point> = None; + let mut min_dist_sq: f64 = f64::MAX; + + // PathSegのバリアントからジオメトリ型(Line, CubicBez)を抽出 + for segment in bez_path.segments() { + + let new_closest_point = match segment { + // Line構造体を抽出 + PathSeg::Line(line) => { + // nearest_param の代わりに nearest を使用し、accuracy に 1.0 (デフォルト値) を渡す + let nearest_result = line.nearest(target_point, 1.0); + let dist_sq = nearest_result.distance_sq; + + if dist_sq < min_dist_sq { + min_dist_sq = dist_sq; + // nearest_result.point は非公開なので、eval(param) を使用する + Some(line.eval(nearest_result.t)) + } else { + None + } + }, + // CubicBez構造体を抽出 + PathSeg::Cubic(cubic_bez) => { + let nearest_result = cubic_bez.nearest(target_point, 1.0); + let dist_sq = nearest_result.distance_sq; + + if dist_sq < min_dist_sq { + min_dist_sq = dist_sq; + Some(cubic_bez.eval(nearest_result.t)) + } else { + None + } + }, + // Quad Bezier (サポート外) およびその他のバリアントはスキップ + _ => None, + }; + + if new_closest_point.is_some() { + closest_point = new_closest_point; } } - // 吸着範囲外、または計算不能 - None + // 4. 結果の返却 + if let Some(p) = closest_point { + return Some(NearestPointResult { x: p.x, y: p.y }); + } else { + return Some(NearestPointResult {x:500.0,y:500.0}); + } + + return Some(NearestPointResult { x: 0.0, y: 0.0 }) +} + +// WASMの初期化関数 (必須) +#[wasm_bindgen(start)] +pub fn main_js() -> Result<(), JsValue> { + Ok(()) }