diff --git a/src/data/coords/ellipseCoords.ts b/src/data/coords/ellipseCoords.ts index 92025155237baa53cb102272e3c9f6463ad235f1..0408a0528de8c745efbe87434db9a59bcbb99152 100644 --- a/src/data/coords/ellipseCoords.ts +++ b/src/data/coords/ellipseCoords.ts @@ -19,8 +19,8 @@ export class EllipseCoords implements Coords { new paper.Size(2 * this.radiusX, 2 * this.radiusY) ) ); - path.rotate(this.angle); path.remove(); + path.rotate(this.angle); return path; } diff --git a/src/data/coords/transform/ellipseFitter.ts b/src/data/coords/transform/ellipseFitter.ts index 73d79198d0727bf9bfe82306305779b212fc7393..7ce261be247ace7a43e00f3b115826e7c31d9ea3 100644 --- a/src/data/coords/transform/ellipseFitter.ts +++ b/src/data/coords/transform/ellipseFitter.ts @@ -6,12 +6,14 @@ import {Coords} from "../coords"; import * as paper from "paper"; import {types} from "sass"; import Color = types.Color; +import {Transformation} from "./transformation"; +import {PathCoords} from "../pathCoords"; const HALFPI : number= 1.5707963267949; /** * Adaptation de EllipseFitter.java de ImageJ */ -export class EllipseFitter { +export class EllipseFitter implements Transformation<PathCoords, Coords>{ private bitCount : number = 0; private xsum : number = 0; @@ -31,7 +33,7 @@ export class EllipseFitter { private u11 : number = 0; //central moments private record : boolean = false; - public getFittingEllipse(coords : Coords) : EllipseCoords { + transform(from: Coords): EllipseCoords { /** X centroid */ let xCenter : number; @@ -52,7 +54,7 @@ export class EllipseFitter { let theta : number; - const path = coords.toPath(); + const path = from.toPath(); let bounds = path.bounds; this.left = Math.round(bounds.x); this.top = Math.round(bounds.y); @@ -111,7 +113,7 @@ export class EllipseFitter { xCenter = this.left + xoffset + 0.5; yCenter = this.top + yoffset + 0.5; - return new EllipseCoords(new paper.Point(xCenter, yCenter), major / 2, minor / 2, angle); + return new EllipseCoords(new paper.Point(xCenter, yCenter), major / 2, minor / 2, -angle); } private computeSums (path : paper.Path) : void { diff --git a/src/data/coords/transform/transformation.ts b/src/data/coords/transform/transformation.ts new file mode 100644 index 0000000000000000000000000000000000000000..95310366a03e433243251c944b16c3c1cdd28e0d --- /dev/null +++ b/src/data/coords/transform/transformation.ts @@ -0,0 +1,11 @@ + +import {Coords} from "../coords"; + +/** + * Interface de transformation de coordonnées + */ +export interface Transformation<FROM extends Coords, TO extends Coords> { + + transform(from: FROM): TO; + +} \ No newline at end of file diff --git a/src/data/dataExporter.ts b/src/data/dataExporter.ts index c308b10b216f6bd35fcc2cb15e733cb56c5e8ef7..705d8a18bfe500a7fbf122a60fb6e57710d15f57 100644 --- a/src/data/dataExporter.ts +++ b/src/data/dataExporter.ts @@ -3,7 +3,6 @@ */ import {LabData} from "../lab"; import {EllipseFitter} from "./coords/transform/ellipseFitter"; -import {PathCoords} from "./coords/pathCoords"; import {Coords} from "./coords/coords"; import {MathUtils} from "../utils/mathUtils"; @@ -36,7 +35,7 @@ export class DataExporter { let linearScale = labData.rulerCoords.distance() / labData.rulerTickCount; // pixels/cm let areaScale = Math.pow(linearScale, 2); - let fittingEllipse = new EllipseFitter().getFittingEllipse(coords); + let fittingEllipse = new EllipseFitter().transform(coords); let line = 1; let label = labData.filename; diff --git a/src/instruments/blobMask.ts b/src/instruments/blobMask.ts index fae0ec2167f56daace4f7e4af3f2fcaa9b0f2b87..53d95742559a7d14e11c1da5b07f4b5a276c095b 100644 --- a/src/instruments/blobMask.ts +++ b/src/instruments/blobMask.ts @@ -1,7 +1,7 @@ import * as paper from "paper"; import {AbstractInstrument, Handle, Instrument} from "./instrument"; import {PathCoords} from "../data/coords/pathCoords"; -import {Lab} from "../lab"; +import {DEBUG_MODE, Lab} from "../lab"; import {EllipseFitter} from "../data/coords/transform/ellipseFitter"; /** @@ -9,6 +9,16 @@ import {EllipseFitter} from "../data/coords/transform/ellipseFitter"; */ export class BlobMask extends AbstractInstrument<PathCoords> implements Instrument { + /** + * Appelé lorsque le tracé est fermé + */ + public onClosed : () => void = () => {}; + + /** + * Appelé lorsque le tracé s'ouvre + */ + public onOpened : () => void = () => {}; + public constructor(protected lab : Lab, coords : PathCoords) { super(lab, coords, [ new Handle("startHandle", true), @@ -19,11 +29,12 @@ export class BlobMask extends AbstractInstrument<PathCoords> implements Instrume let line = coords.path.clone(); group.addChild(line); - // Affiche la fitted ellipse - // if(line.length > 10) { - // const pathCoords = new PathCoords(line.clone()); - // group.addChild(new EllipseFitter().getFittingEllipse(pathCoords).toPath()); - // } + if(DEBUG_MODE) { + if(line.length > 10) { + const pathCoords = new PathCoords(line.clone()); + group.addChild(new EllipseFitter().transform(pathCoords).toPath()); + } + } } @@ -59,7 +70,13 @@ export class BlobMask extends AbstractInstrument<PathCoords> implements Instrume if(!this.active) { return true; } - this.coords.path.add(event.point); + if(!this.coords.isClosed()) { // Une fois la boucle fermée, on ne peut plus ajouter de + this.coords.path.add(event.point) + if(this.coords.isClosed()) { + this.onClosed(); + } + } + this.refresh(); return true; } @@ -78,15 +95,26 @@ export class BlobMask extends AbstractInstrument<PathCoords> implements Instrume * Supprime quelques derniers points */ public undo() { - this.coords.path.removeSegments(Math.max(this.coords.path.segments.length - 5, 0)); - this.refresh(); + this._undo(Math.max(this.coords.path.segments.length - 5, 0)); } /** - * Supprime touy + * Supprime tout */ public undoAll() { - this.coords.path.removeSegments(); + this._undo(0); + } + + /** + * En charge de la suppression + */ + private _undo(from : number) { + let wasClosed = this.coords.isClosed(); + this.coords.path.removeSegments(from); + if(wasClosed) { + this.onOpened(); + } this.refresh(); } + } \ No newline at end of file diff --git a/src/lab.tsx b/src/lab.tsx index 59ad974bd74a0b1cfe36e16026194b370fc729cf..a24f7202f1e667ba8dbb740c880fcaad62e47a66 100644 --- a/src/lab.tsx +++ b/src/lab.tsx @@ -10,12 +10,16 @@ import {PetriDish} from "./instruments/petriDish"; import {DrawBlobMaskStep} from "./steps/drawBlobMaskStep"; import {BlobMask} from "./instruments/blobMask"; import {VectorCoords} from "./data/coords/vectorCoords"; -import {CircleCoords} from "./data/coords/circleCoords"; import {PathCoords} from "./data/coords/pathCoords"; import {DownloadStep} from "./steps/downloadStep"; import {PaperUtils} from "./utils/paperUtils"; import {EllipseCoords} from "./data/coords/ellipseCoords"; +/** + * Debug mode (ou pas) + */ +export const DEBUG_MODE = false; + export interface LabData { pictureSize : paper.Size, @@ -210,14 +214,14 @@ export class Lab extends React.Component<{}> { * Plus de zoom */ public zoomIn(target? : paper.Point) : void { - this.zoom(1.05, target); + this.zoom(1.10, target); } /** * Moins de zoom */ public zoomOut(target? : paper.Point) : void { - this.zoom(0.95, target); + this.zoom(0.90, target); } /** @@ -259,7 +263,6 @@ export class Lab extends React.Component<{}> { this.blobMask.refresh(); } - render(): React.ReactNode { return <Container fluid={true} className={"vh-100 d-flex flex-column"}> <Navbar bg="light" expand="lg" className={"p-0"}> diff --git a/src/steps/downloadStep.tsx b/src/steps/downloadStep.tsx index 3a1f1c08517d4d922c79e1225b04aee0ff4f9044..98fdc45c3385b02cdb59b009395314f911c26a62 100644 --- a/src/steps/downloadStep.tsx +++ b/src/steps/downloadStep.tsx @@ -19,7 +19,7 @@ export class DownloadButton extends React.Component<DownloadButtonProps, any> { render() : React.ReactNode { let faIcon = this.props.downloading ? "fa-solid fas fa-cog fa-spin" : "fa-solid fa-download"; - let key = this.props.downloading ? "dl" : "notdl"; + let key = this.props.downloading ? "downloading" : "notDownloading"; return <Button key={key} onClick={this.props.onClick} disabled={this.props.disabled || this.props.downloading} variant={"primary"} size={"sm"}> <i className={faIcon}></i> </Button> diff --git a/src/steps/drawBlobMaskStep.tsx b/src/steps/drawBlobMaskStep.tsx index 0fe00591b6dec5e78443446b07073c72e5d5be08..420681fa34db1b190892ad0c79ecb547339aeffa 100644 --- a/src/steps/drawBlobMaskStep.tsx +++ b/src/steps/drawBlobMaskStep.tsx @@ -2,13 +2,20 @@ import {Step, StepProps, StepState} from "./step"; import * as React from "react"; import {Alert, Button} from "react-bootstrap"; + +interface DrawBlobMaskStepState extends StepState { + + closed: boolean; + +} + /** - * Etape de placement de la règle + * Étape de placement de la boîte de petri */ -export class DrawBlobMaskStep extends Step<StepState> { +export class DrawBlobMaskStep extends Step<DrawBlobMaskStepState> { public constructor(props : StepProps) { - super(props, { active: false, activable : false }); + super(props, { active: false, activable : false, closed : false }); } canBeActivated(): boolean { @@ -17,6 +24,12 @@ export class DrawBlobMaskStep extends Step<StepState> { onActivation(): void { this.props.lab.blobMask.activate(); + this.props.lab.blobMask.onClosed = () => { + this.setState({closed: true }); + }; + this.props.lab.blobMask.onOpened = () => { + this.setState({closed: false }); + }; this.props.lab.zoomOn( this.props.lab.data.petriDishCoords.bounds(), 0.05); } @@ -33,7 +46,9 @@ export class DrawBlobMaskStep extends Step<StepState> { Revenir en arrière : <Button className={"me-2"} size={"sm"} disabled={!this.state.active} onClick={() => this.props.lab.blobMask.undo()}><i className="fa-solid fa-delete-left"></i></Button> Tout effacer : <Button className={"me-2"} size={"sm"} disabled={!this.state.active} onClick={() => this.props.lab.blobMask.undoAll()}><i className="fa-solid fa-trash-can"></i></Button> </Alert> - <Button variant={"success"} disabled={!this.state.active} onClick={this.terminate.bind(this)}>Fini !</Button> + <Button className={"col-3"} variant={"success"} disabled={!this.state.active || !this.state.closed} onClick={this.terminate.bind(this)}> + <span hidden={!this.state.closed}><i className="fa-solid fa-hands-clapping fa-beat-fade me-2"></i></span>Fini ! + </Button> </div> </div> } diff --git a/src/steps/placePetriDishStep.tsx b/src/steps/placePetriDishStep.tsx index c39ed05f70629bbe12d0e6004076f2bdcfd917d5..fa2481698287fbf58a9c77a3ccd26bd9ab4ac847 100644 --- a/src/steps/placePetriDishStep.tsx +++ b/src/steps/placePetriDishStep.tsx @@ -33,7 +33,7 @@ export class PlacePetriDishStep extends Step<StepState> { <div> <Alert show={!this.state.activable} variant="warning" className={"p-1"}>Veuillez charger une photo.</Alert> <p>Déplacez et redimensionnez le cercle à l'aide des poignées blanches pour placer la boîte de petri.</p> - <p>Appuyez ici <Button disabled={!this.state.active} onClick={this.zoomOnPetriDishCenter.bind(this)} size={"sm"}><i className={"fa-solid fa-magnifying-glass-location"}></i></Button> pour placer la boîte de petri avec précision.</p> + <Alert variant={"light"} className={"p-2"}><i className="ms-1 me-1 fa-solid fa-circle-info"></i>Cliquez ici <Button disabled={!this.state.active} onClick={this.zoomOnPetriDishCenter.bind(this)} size={"sm"}><i className={"fa-solid fa-magnifying-glass-location"}></i></Button> pour placer la boîte de petri avec précision.</Alert> <Button variant={"success"} disabled={!this.state.active} onClick={this.terminate.bind(this)}>C'est fait !</Button> </div> </div> diff --git a/src/steps/rulerStep.tsx b/src/steps/rulerStep.tsx index bc3d10de77cb613943ec8a615c7e57585eb8c720..5c0a4c2c433ef44c4020ab69a3685b0a0eea97a1 100644 --- a/src/steps/rulerStep.tsx +++ b/src/steps/rulerStep.tsx @@ -33,7 +33,7 @@ export class RulerStep extends Step<StepState> { <div> <Alert show={!this.state.activable} variant="warning" className={"p-1"}>Veuillez charger une photo.</Alert> <p>Positionnez la règle sur la photo. La règle doit couvrir 9 cm.</p> - <p>Appuyez ici <Button disabled={!this.state.active} onClick={this.zoomOnRuler.bind(this)} size={"sm"}><i className={"fa-solid fa-magnifying-glass-location"}></i></Button> pour placer la règle avec précision.</p> + <Alert variant={"light"} className={"p-2"}><i className="ms-1 me-1 fa-solid fa-circle-info"></i> Appuyez ici <Button disabled={!this.state.active} onClick={this.zoomOnRuler.bind(this)} size={"sm"}><i className={"fa-solid fa-magnifying-glass-location"}></i></Button> pour placer la règle avec précision.</Alert> <Button variant={"success"} disabled={!this.state.active} onClick={this.terminate.bind(this)}>Terminé !</Button> </div> </div> diff --git a/src/steps/step.tsx b/src/steps/step.tsx index ac9c7260a08c8a3725a820ef716c0eb7e5701f2a..aac5302b2fa41285d43ccb6012f3b70507bd475c 100644 --- a/src/steps/step.tsx +++ b/src/steps/step.tsx @@ -23,7 +23,7 @@ export interface StepState { */ export abstract class Step<S extends StepState> extends React.Component<StepProps, S> { - public onTerminated : (stepComponent : Step<S>) => void = () => { console.log("Aïe"); }; + public onTerminated : (stepComponent : Step<S>) => void = () => { }; protected constructor(props : StepProps, defaultState : S) { super(props);